zhibin 6 anni fa
commit
c528ce5140
65 ha cambiato i file con 18285 aggiunte e 0 eliminazioni
  1. 18 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 5 0
      .eslintignore
  4. 29 0
      .eslintrc.js
  5. 17 0
      .gitignore
  6. 10 0
      .postcssrc.js
  7. 30 0
      README.md
  8. 41 0
      build/build.js
  9. 54 0
      build/check-versions.js
  10. BIN
      build/logo.png
  11. 101 0
      build/utils.js
  12. 22 0
      build/vue-loader.conf.js
  13. 92 0
      build/webpack.base.conf.js
  14. 95 0
      build/webpack.dev.conf.js
  15. 149 0
      build/webpack.prod.conf.js
  16. 7 0
      config/dev.env.js
  17. 84 0
      config/index.js
  18. 4 0
      config/prod.env.js
  19. 7 0
      config/test.env.js
  20. 13 0
      index.html
  21. 14684 0
      package-lock.json
  22. 90 0
      package.json
  23. 22 0
      src/App.vue
  24. BIN
      src/assets/images/bg.jpg
  25. BIN
      src/assets/images/icons.png
  26. BIN
      src/assets/images/logo.png
  27. BIN
      src/assets/images/代理-用户明细.psd
  28. BIN
      src/assets/images/代理-用户查询.psd
  29. BIN
      src/assets/logo.png
  30. 151 0
      src/assets/style/index.css
  31. 54 0
      src/components/date/index.vue
  32. 44 0
      src/components/date/style.css
  33. 240 0
      src/components/date/util.js
  34. 56 0
      src/components/dateRange/index.vue
  35. 50 0
      src/components/dateRange/style.css
  36. 66 0
      src/components/icons/index.vue
  37. 207 0
      src/data/index.js
  38. 27 0
      src/main.js
  39. 247 0
      src/pages/case/details/index.vue
  40. 18 0
      src/pages/case/details/style.css
  41. 118 0
      src/pages/case/list/index.vue
  42. 49 0
      src/pages/case/list/style.css
  43. 63 0
      src/pages/home/index.vue
  44. 151 0
      src/pages/layout/index.vue
  45. 152 0
      src/pages/layout/style.css
  46. 102 0
      src/pages/login/index.css
  47. 94 0
      src/pages/login/index.vue
  48. 193 0
      src/pages/news/details/index.vue
  49. 18 0
      src/pages/news/details/style.css
  50. 119 0
      src/pages/news/list/index.vue
  51. 49 0
      src/pages/news/list/style.css
  52. 77 0
      src/router/index.js
  53. 29 0
      src/util/cookie.js
  54. 34 0
      src/util/extend.js
  55. 88 0
      src/util/http.js
  56. 15 0
      src/util/prompt.js
  57. 0 0
      static/.gitkeep
  58. 27 0
      test/e2e/custom-assertions/elementCount.js
  59. 46 0
      test/e2e/nightwatch.conf.js
  60. 48 0
      test/e2e/runner.js
  61. 19 0
      test/e2e/specs/test.js
  62. 7 0
      test/unit/.eslintrc
  63. 30 0
      test/unit/jest.conf.js
  64. 3 0
      test/unit/setup.js
  65. 11 0
      test/unit/specs/HelloWorld.spec.js

+ 18 - 0
.babelrc

@@ -0,0 +1,18 @@
+{
+  "presets": [
+    ["env", {
+      "modules": false,
+      "targets": {
+        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
+      }
+    }],
+    "stage-2"
+  ],
+  "plugins": ["transform-vue-jsx", "transform-runtime"],
+  "env": {
+    "test": {
+      "presets": ["env", "stage-2"],
+      "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
+    }
+  }
+}

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 5 - 0
.eslintignore

@@ -0,0 +1,5 @@
+/build/
+/config/
+/dist/
+/*.js
+/test/unit/coverage/

+ 29 - 0
.eslintrc.js

@@ -0,0 +1,29 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint'
+  },
+  env: {
+    browser: true,
+  },
+  extends: [
+    // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
+    // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
+    'plugin:vue/essential', 
+    // https://github.com/standard/standard/blob/master/docs/RULES-en.md
+    'standard'
+  ],
+  // required to lint *.vue files
+  plugins: [
+    'vue'
+  ],
+  // add your custom rules here
+  rules: {
+    // allow async-await
+    'generator-star-spacing': 'off',
+    // allow debugger during development
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+  }
+}

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+.DS_Store
+node_modules/
+/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+/test/unit/coverage/
+/test/e2e/reports/
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 10 - 0
.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": {}
+  }
+}

+ 30 - 0
README.md

@@ -0,0 +1,30 @@
+# y
+
+> 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
+
+# run unit tests
+npm run unit
+
+# run e2e tests
+npm run e2e
+
+# run all tests
+npm test
+```
+
+For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

+ 41 - 0
build/build.js

@@ -0,0 +1,41 @@
+'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, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
+      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'
+    ))
+  })
+})

+ 54 - 0
build/check-versions.js

@@ -0,0 +1,54 @@
+'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)
+  }
+}

BIN
build/logo.png


+ 101 - 0
build/utils.js

@@ -0,0 +1,101 @@
+'use strict'
+const path = require('path')
+const config = require('../config')
+const ExtractTextPlugin = require('extract-text-webpack-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 = options.usePostCSS ? [cssLoader, postcssLoader] : [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) {
+  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')
+    })
+  }
+}

+ 22 - 0
build/vue-loader.conf.js

@@ -0,0 +1,22 @@
+'use strict'
+const utils = require('./utils')
+const config = require('../config')
+const isProduction = process.env.NODE_ENV === 'production'
+const sourceMapEnabled = isProduction
+  ? config.build.productionSourceMap
+  : config.dev.cssSourceMap
+
+module.exports = {
+  loaders: utils.cssLoaders({
+    sourceMap: sourceMapEnabled,
+    extract: isProduction
+  }),
+  cssSourceMap: sourceMapEnabled,
+  cacheBusting: config.dev.cacheBusting,
+  transformToRequire: {
+    video: ['src', 'poster'],
+    source: 'src',
+    img: 'src',
+    image: 'xlink:href'
+  }
+}

+ 92 - 0
build/webpack.base.conf.js

@@ -0,0 +1,92 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+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: {
+      'vue$': 'vue/dist/vue.esm.js',
+      '@': resolve('src'),
+    }
+  },
+  module: {
+    rules: [
+      ...(config.dev.useEslint ? [createLintingRule()] : []),
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader',
+        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        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]')
+        }
+      }
+    ]
+  },
+  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'
+  }
+}

+ 95 - 0
build/webpack.dev.conf.js

@@ -0,0 +1,95 @@
+'use strict'
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const path = require('path')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+const portfinder = require('portfinder')
+
+const HOST = process.env.HOST
+const PORT = process.env.PORT && Number(process.env.PORT)
+
+const devWebpackConfig = merge(baseWebpackConfig, {
+  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: {
+      rewrites: [
+        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
+      ],
+    },
+    hot: true,
+    contentBase: false, // since we use CopyWebpackPlugin.
+    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(),
+    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
+    new webpack.NoEmitOnErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.html',
+      inject: true
+    }),
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.dev.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ]
+})
+
+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)
+    }
+  })
+})

+ 149 - 0
build/webpack.prod.conf.js

@@ -0,0 +1,149 @@
+'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 ExtractTextPlugin = require('extract-text-webpack-plugin')
+const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+
+const env = process.env.NODE_ENV === 'testing'
+  ? require('../config/test.env')
+  : require('../config/prod.env')
+
+const webpackConfig = merge(baseWebpackConfig, {
+  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].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 UglifyJsPlugin({
+      uglifyOptions: {
+        compress: {
+          warnings: false
+        }
+      },
+      sourceMap: config.build.productionSourceMap,
+      parallel: true
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash].css'),
+      // Setting the following option to `false` will not extract CSS from codesplit chunks.
+      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
+      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
+      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
+      allChunks: true,
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin({
+      cssProcessorOptions: config.build.productionSourceMap
+        ? { safe: true, map: { inline: false } }
+        : { 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: process.env.NODE_ENV === 'testing'
+        ? 'index.html'
+        : 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'
+    }),
+    // keep module.id stable when vendor modules does not change
+    new webpack.HashedModuleIdsPlugin(),
+    // enable scope hoisting
+    new webpack.optimize.ModuleConcatenationPlugin(),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks (module) {
+        // 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',
+      minChunks: Infinity
+    }),
+    // This instance extracts shared chunks from code splitted chunks and bundles them
+    // in a separate chunk, similar to the vendor chunk
+    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'app',
+      async: 'vendor-async',
+      children: true,
+      minChunks: 3
+    }),
+
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.build.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ]
+})
+
+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.bundleAnalyzerReport) {
+  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+module.exports = webpackConfig

+ 7 - 0
config/dev.env.js

@@ -0,0 +1,7 @@
+'use strict'
+const merge = require('webpack-merge')
+const prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"'
+})

+ 84 - 0
config/index.js

@@ -0,0 +1,84 @@
+'use strict'
+// Template version: 1.3.1
+// see http://vuejs-templates.github.io/webpack for documentation.
+
+const path = require('path')
+
+module.exports = {
+  dev: {
+
+    // Paths
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    proxyTable: {
+      '/': {
+        target: 'http://120.24.64.23:3000/',
+        changeOrigin: true,
+        pathRewrite: {
+          ['^/']: ''
+        }
+      }
+    },
+
+    // Various Dev Server settings
+    host: 'localhost', // can be overwritten by process.env.HOST
+    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
+    autoOpenBrowser: false,
+    errorOverlay: true,
+    notifyOnErrors: true,
+    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-module-eval-source-map',
+
+    // If you have problems debugging vue-files in devtools,
+    // set this to false - it *may* help
+    // https://vue-loader.vuejs.org/en/options.html#cachebusting
+    cacheBusting: true,
+
+    cssSourceMap: true
+  },
+
+  build: {
+    // Template for index.html
+    index: path.resolve(__dirname, '../dist/index.html'),
+
+    // Paths
+    assetsRoot: path.resolve(__dirname, '../dist'),
+    assetsSubDirectory: 'static',
+    assetsPublicPath: './',
+
+    /**
+     * Source Maps
+     */
+
+    productionSourceMap: true,
+    // 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
+  }
+}

+ 4 - 0
config/prod.env.js

@@ -0,0 +1,4 @@
+'use strict'
+module.exports = {
+  NODE_ENV: '"production"'
+}

+ 7 - 0
config/test.env.js

@@ -0,0 +1,7 @@
+'use strict'
+const merge = require('webpack-merge')
+const devEnv = require('./dev.env')
+
+module.exports = merge(devEnv, {
+  NODE_ENV: '"testing"'
+})

File diff suppressed because it is too large
+ 13 - 0
index.html


File diff suppressed because it is too large
+ 14684 - 0
package-lock.json


+ 90 - 0
package.json

@@ -0,0 +1,90 @@
+{
+  "name": "y",
+  "version": "1.0.0",
+  "description": "A Vue.js project",
+  "author": "",
+  "private": true,
+  "scripts": {
+    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+    "start": "npm run dev",
+    "unit": "jest --config test/unit/jest.conf.js --coverage",
+    "e2e": "node test/e2e/runner.js",
+    "test": "npm run unit && npm run e2e",
+    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
+    "build": "node build/build.js"
+  },
+  "dependencies": {
+    "axios": "^0.18.0",
+    "element-ui": "^2.4.3",
+    "vue": "^2.5.2",
+    "vue-router": "^3.0.1",
+    "wangeditor": "^3.1.1"
+  },
+  "devDependencies": {
+    "autoprefixer": "^7.1.2",
+    "babel-core": "^6.22.1",
+    "babel-eslint": "^8.2.1",
+    "babel-helper-vue-jsx-merge-props": "^2.0.3",
+    "babel-jest": "^21.0.2",
+    "babel-loader": "^7.1.1",
+    "babel-plugin-dynamic-import-node": "^1.2.0",
+    "babel-plugin-syntax-jsx": "^6.18.0",
+    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
+    "babel-plugin-transform-runtime": "^6.22.0",
+    "babel-plugin-transform-vue-jsx": "^3.5.0",
+    "babel-preset-env": "^1.3.2",
+    "babel-preset-stage-2": "^6.22.0",
+    "babel-register": "^6.22.0",
+    "chalk": "^2.0.1",
+    "chromedriver": "^2.27.2",
+    "copy-webpack-plugin": "^4.0.1",
+    "cross-spawn": "^5.0.1",
+    "css-loader": "^0.28.0",
+    "eslint": "^4.15.0",
+    "eslint-config-standard": "^10.2.1",
+    "eslint-friendly-formatter": "^3.0.0",
+    "eslint-loader": "^1.7.1",
+    "eslint-plugin-import": "^2.7.0",
+    "eslint-plugin-node": "^5.2.0",
+    "eslint-plugin-promise": "^3.4.0",
+    "eslint-plugin-standard": "^3.0.1",
+    "eslint-plugin-vue": "^4.0.0",
+    "extract-text-webpack-plugin": "^3.0.0",
+    "file-loader": "^1.1.4",
+    "friendly-errors-webpack-plugin": "^1.6.1",
+    "html-webpack-plugin": "^2.30.1",
+    "jest": "^22.0.4",
+    "jest-serializer-vue": "^0.3.0",
+    "nightwatch": "^0.9.12",
+    "node-notifier": "^5.1.2",
+    "optimize-css-assets-webpack-plugin": "^3.2.0",
+    "ora": "^1.2.0",
+    "portfinder": "^1.0.13",
+    "postcss-import": "^11.0.0",
+    "postcss-loader": "^2.0.8",
+    "postcss-url": "^7.2.1",
+    "rimraf": "^2.6.0",
+    "selenium-server": "^3.0.1",
+    "semver": "^5.3.0",
+    "shelljs": "^0.7.6",
+    "uglifyjs-webpack-plugin": "^1.1.1",
+    "url-loader": "^0.5.8",
+    "vue-jest": "^1.0.2",
+    "vue-loader": "^13.3.0",
+    "vue-style-loader": "^3.0.1",
+    "vue-template-compiler": "^2.5.2",
+    "webpack": "^3.6.0",
+    "webpack-bundle-analyzer": "^2.9.0",
+    "webpack-dev-server": "^2.9.1",
+    "webpack-merge": "^4.1.0"
+  },
+  "engines": {
+    "node": ">= 6.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 22 - 0
src/App.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="layout" id="ctrl">
+    <router-view class="view" />
+  </div>
+</template>
+
+<script>
+import '@/assets/style/index.css'
+
+export default {
+  name: 'app'
+}
+</script>
+
+<style>
+  html, body {
+    height: 100%;
+  }
+  .layout {
+    height: 100%;
+  }
+</style>

BIN
src/assets/images/bg.jpg


BIN
src/assets/images/icons.png


BIN
src/assets/images/logo.png


BIN
src/assets/images/代理-用户明细.psd


BIN
src/assets/images/代理-用户查询.psd


BIN
src/assets/logo.png


+ 151 - 0
src/assets/style/index.css

@@ -0,0 +1,151 @@
+/* reset */
+html,body,h1,h2,h3,h4,h5,h6,div,dl,dt,dd,ul,ol,li,p,blockquote,pre,hr,figure,table,caption,th,td,form,fieldset,legend,input,button,textarea,menu{margin:0;padding:0;}
+header,footer,section,article,aside,nav,hgroup,address,figure,figcaption,menu,details{display:block;}
+table{border-collapse:collapse;border-spacing:0;}
+caption,th{text-align:left;font-weight:normal;}
+html,body,fieldset,img,iframe,abbr{border:0;}
+i,cite,em,var,address,dfn{font-style:normal;}
+[hidefocus],summary{outline:0;}
+li{list-style:none;}
+h1,h2,h3,h4,h5,h6,small{font-size:100%;}
+sup,sub{font-size:83%;}
+pre,code,kbd,samp{font-family:inherit;}
+q:before,q:after{content:none;}
+textarea{overflow:auto;resize:none;}
+label,summary{cursor:default;}
+a,button{cursor:pointer;}
+h1,h2,h3,h4,h5,h6,em,strong,b{font-weight:bold;}
+del,ins,u,s,a,a:hover{text-decoration:none;}
+body,textarea,input,button,select,keygen,legend{font:12px/1.14 'Microsoft YaHei',\5b8b\4f53;color:#333;outline:0;}
+body{background:#fff;}
+a,a:hover{color:#fff;}
+*{box-sizing: border-box}
+
+.el-input.is-active .el-input__inner, .el-input__inner:focus {
+  border-color: #09aafe !important;
+}
+.el-pager li.active {
+  color: #09aafe !important;
+}
+.el-checkbox__input.is-checked+.el-checkbox__label {
+  color: #09aafe !important;
+}
+
+.el-checkbox__input.is-checked .el-checkbox__inner, .el-checkbox__input.is-indeterminate .el-checkbox__inner {
+  background-color: #09aafe !important;
+  border-color: #09aafe !important;
+}
+
+.el-checkbox__inner:hover {
+  border-color: #09aafe !important;
+}
+
+.el-loading-spinner .path  {
+  stroke: #09aafe !important
+}
+
+.table {
+  border: none !important;
+}
+
+.el-table__footer-wrapper tbody td, .el-table__header-wrapper tbody td {
+  border-left: none !important;
+  border-right: none !important;
+  background-color: #fff !important;
+}
+table th,
+table td {
+  border-left: none !important;
+  border-right: none !important;
+  text-align: center !important;
+}
+.table.el-table--enable-row-hover .el-table__body tr:hover>td {
+  background-color: #fff;
+}
+.el-table--border::after, .el-table--group::after {
+  display: none !important
+}
+
+.table.el-table thead.is-group th,
+.table.el-table thead th {
+  background-color: #fff;
+  color: #999999;
+  font-weight: bold;
+  font-size: 14px;
+  text-align: center !important;
+}
+
+.table .el-table__footer td {
+  font-weight: bold;
+  color: #333;
+  text-align: center !important;
+  font-style: italic;
+}
+
+.el-button:focus, .el-button:hover {
+  color: #fff !important;
+  border-color: #09aafe !important;
+  background-color: #09aafe !important;
+}
+
+.el-table .descending .sort-caret.descending {
+  border-top-color: #09aafe !important;
+}
+
+.el-table .ascending .sort-caret.ascending {
+    border-bottom-color: #09aafe !important;
+}
+
+
+.cover-uploader {
+  position: relative;
+}
+
+.cover-uploader .el-upload {
+  position: absolute;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  overflow: hidden;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.cover-uploader .el-upload:hover {
+  border-color: #409EFF;
+}
+
+
+.cover-content {
+  position: absolute;
+  width: 100%;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #999999;
+}
+
+.cover-content p {
+  font-size: 12px;
+  line-height: 25px;
+  color: inherit
+}
+.cover-uploader-icon {
+  font-size: 38px;
+  color: inherit
+}
+
+.cover {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+.w-e-toolbar {
+  flex-wrap: wrap;
+}
+
+a {
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
+}

+ 54 - 0
src/components/date/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <!-- 年月日时分秒 -->
+  <!-- 点击接受时间的时候可以选一个日期,然后点击办结时间的时候也可以选一个日期,而且开始时间不能大于办结时间 -->
+  <!-- 除了双选,还可以单选,就是可以选其中任何一个 -->
+  <div class="ddd-layout" @click="grentLayout" ref="ddlayout">
+    <el-date-picker
+      class="date"
+      v-model="value2"
+      align="right"
+      type="date"
+      placeholder="请选择接收时间">
+    </el-date-picker>
+    <el-date-picker
+      class="date"
+      v-model="value2"
+      align="right"
+      type="date"
+      placeholder="请选择结办时间">
+    </el-date-picker>
+  </div>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      value2: '',
+      doms: []
+    }
+  },
+  methods: {
+    grentLayout () {
+      let interval = setInterval(() => {
+        let doms = document.querySelectorAll('.el-picker-panel.el-date-picker.el-popper')
+        if (doms.length > 1) {
+          Array.from(this.doms).forEach(dom => {
+            dom.parentNode.removeChild(dom)
+          })
+          this.doms = []
+        } else if (doms.length > 0) {
+          let dom = doms[0]
+          clearInterval(interval)
+          this.$refs.ddlayout.appendChild(dom)
+          this.doms.push(dom)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+@import url('./style.css');
+</style>

+ 44 - 0
src/components/date/style.css

@@ -0,0 +1,44 @@
+.ddd-layout {
+  width: 50%;
+  display: flex;
+  position: relative;
+}
+
+.ddd-layout .date {
+  width: 50%;
+}
+
+.ddd-layout .el-picker-panel.el-date-picker.el-popper {
+  width: 100%;
+  top: 100% !important;
+  left: 0 !important;
+  margin-top: 0;
+}
+
+.ddd-layout .el-date-picker .el-picker-panel__content {
+  margin: 5px;
+  width: calc(100% - 10px)
+}
+
+.ddd-layout .el-popper .popper__arrow {
+  display: none;
+}
+
+.ddd-layout .el-date-picker tr:not(.el-date-table__row) {
+  /* display: none; */
+}
+
+.ddd-layout .el-date-picker__header-label {
+  font-size: 12px;
+}
+
+.ddd-layout .el-year-table td,
+.ddd-layout .el-month-table td {
+  padding: 3px;
+}
+
+.ddd-layout .el-date-table td {
+  padding: 0;
+  width: auto;
+  height: auto;
+}

+ 240 - 0
src/components/date/util.js

@@ -0,0 +1,240 @@
+import dateUtil from 'element-ui/src/utils/date';
+import { t } from 'element-ui/src/locale';
+
+const weeks = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
+const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
+const getI18nSettings = () => {
+  return {
+    dayNamesShort: weeks.map(week => t(`el.datepicker.weeks.${week}`)),
+    dayNames: weeks.map(week => t(`el.datepicker.weeks.${week}`)),
+    monthNamesShort: months.map(month => t(`el.datepicker.months.${month}`)),
+    monthNames: months.map((month, index) => t(`el.datepicker.month${index + 1}`)),
+    amPm: ['am', 'pm']
+  };
+};
+
+const newArray = function (start, end) {
+  let result = [];
+  for (let i = start; i <= end; i++) {
+    result.push(i);
+  }
+  return result;
+};
+
+export const toDate = function (date) {
+  return isDate(date) ? new Date(date) : null;
+};
+
+export const isDate = function (date) {
+  if (date === null || date === undefined) return false;
+  if (isNaN(new Date(date).getTime())) return false;
+  if (Array.isArray(date)) return false; // deal with `new Date([ new Date() ]) -> new Date()`
+  return true;
+};
+
+export const isDateObject = function (val) {
+  return val instanceof Date;
+};
+
+export const formatDate = function (date, format) {
+  date = toDate(date);
+  if (!date) return '';
+  return dateUtil.format(date, format || 'yyyy-MM-dd', getI18nSettings());
+};
+
+export const parseDate = function (string, format) {
+  return dateUtil.parse(string, format || 'yyyy-MM-dd', getI18nSettings());
+};
+
+export const getDayCountOfMonth = function (year, month) {
+  if (month === 3 || month === 5 || month === 8 || month === 10) {
+    return 30;
+  }
+
+  if (month === 1) {
+    if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
+      return 29;
+    } else {
+      return 28;
+    }
+  }
+
+  return 31;
+};
+
+export const getDayCountOfYear = function (year) {
+  const isLeapYear = year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
+  return isLeapYear ? 366 : 365;
+};
+
+export const getFirstDayOfMonth = function (date) {
+  const temp = new Date(date.getTime());
+  temp.setDate(1);
+  return temp.getDay();
+};
+
+// see: https://stackoverflow.com/questions/3674539/incrementing-a-date-in-javascript
+// {prev, next} Date should work for Daylight Saving Time
+// Adding 24 * 60 * 60 * 1000 does not work in the above scenario
+export const prevDate = function (date, amount = 1) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate() - amount);
+};
+
+export const nextDate = function (date, amount = 1) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
+};
+
+export const getStartDateOfMonth = function (year, month) {
+  const result = new Date(year, month, 1);
+  const day = result.getDay();
+
+  if (day === 0) {
+    return prevDate(result, 7);
+  } else {
+    return prevDate(result, day);
+  }
+};
+
+export const getWeekNumber = function (src) {
+  if (!isDate(src)) return null;
+  const date = new Date(src.getTime());
+  date.setHours(0, 0, 0, 0);
+  // Thursday in current week decides the year.
+  date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
+  // January 4 is always in week 1.
+  const week1 = new Date(date.getFullYear(), 0, 4);
+  // Adjust to Thursday in week 1 and count number of weeks from date to week 1.
+  // Rounding should be fine for Daylight Saving Time. Its shift should never be more than 12 hours.
+  return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
+};
+
+export const getRangeHours = function (ranges) {
+  const hours = [];
+  let disabledHours = [];
+
+  (ranges || []).forEach(range => {
+    const value = range.map(date => date.getHours());
+
+    disabledHours = disabledHours.concat(newArray(value[0], value[1]));
+  });
+
+  if (disabledHours.length) {
+    for (let i = 0; i < 24; i++) {
+      hours[i] = disabledHours.indexOf(i) === -1;
+    }
+  } else {
+    for (let i = 0; i < 24; i++) {
+      hours[i] = false;
+    }
+  }
+
+  return hours;
+};
+
+export const range = function (n) {
+  // see https://stackoverflow.com/questions/3746725/create-a-javascript-array-containing-1-n
+  return Array.apply(null, { length: n }).map((_, n) => n);
+};
+
+export const modifyDate = function (date, y, m, d) {
+  return new Date(y, m, d, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
+};
+
+export const modifyTime = function (date, h, m, s) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate(), h, m, s, date.getMilliseconds());
+};
+
+export const modifyWithTimeString = (date, time) => {
+  if (date == null || !time) {
+    return date;
+  }
+  time = parseDate(time, 'HH:mm:ss');
+  return modifyTime(date, time.getHours(), time.getMinutes(), time.getSeconds());
+};
+
+export const clearTime = function (date) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
+};
+
+export const clearMilliseconds = function (date) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), 0);
+};
+
+export const limitTimeRange = function (date, ranges, format = 'HH:mm:ss') {
+  // TODO: refactory a more elegant solution
+  if (ranges.length === 0) return date;
+  const normalizeDate = date => dateUtil.parse(dateUtil.format(date, format), format);
+  const ndate = normalizeDate(date);
+  const nranges = ranges.map(range => range.map(normalizeDate));
+  if (nranges.some(nrange => ndate >= nrange[0] && ndate <= nrange[1])) return date;
+
+  let minDate = nranges[0][0];
+  let maxDate = nranges[0][0];
+
+  nranges.forEach(nrange => {
+    minDate = new Date(Math.min(nrange[0], minDate));
+    maxDate = new Date(Math.max(nrange[1], minDate));
+  });
+
+  const ret = ndate < minDate ? minDate : maxDate;
+  // preserve Year/Month/Date
+  return modifyDate(
+    ret,
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate()
+  );
+};
+
+export const timeWithinRange = function (date, selectableRange, format) {
+  const limitedDate = limitTimeRange(date, selectableRange, format);
+  return limitedDate.getTime() === date.getTime();
+};
+
+export const changeYearMonthAndClampDate = function (date, year, month) {
+  // clamp date to the number of days in `year`, `month`
+  // eg: (2010-1-31, 2010, 2) => 2010-2-28
+  const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
+  return modifyDate(date, year, month, monthDate);
+};
+
+export const prevMonth = function (date) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return month === 0
+    ? changeYearMonthAndClampDate(date, year - 1, 11)
+    : changeYearMonthAndClampDate(date, year, month - 1);
+};
+
+export const nextMonth = function (date) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return month === 11
+    ? changeYearMonthAndClampDate(date, year + 1, 0)
+    : changeYearMonthAndClampDate(date, year, month + 1);
+};
+
+export const prevYear = function (date, amount = 1) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return changeYearMonthAndClampDate(date, year - amount, month);
+};
+
+export const nextYear = function (date, amount = 1) {
+  const year = date.getFullYear();
+  const month = date.getMonth();
+  return changeYearMonthAndClampDate(date, year + amount, month);
+};
+
+export const extractDateFormat = function (format) {
+  return format
+    .replace(/\W?m{1,2}|\W?ZZ/g, '')
+    .replace(/\W?h{1,2}|\W?s{1,3}|\W?a/gi, '')
+    .trim();
+};
+
+export const extractTimeFormat = function (format) {
+  return format
+    .replace(/\W?D{1,2}|\W?Do|\W?d{1,4}|\W?M{1,4}|\W?y{2,4}/g, '')
+    .trim();
+};

+ 56 - 0
src/components/dateRange/index.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="range-layout">
+    <span>2015-01-09</span>
+    <div class="continue" ref="continue">
+      <a class="bar" @mousedown="handleMousedown" :style="{left}">
+      </a>
+    </div>
+    <span>2017-01-09</span>
+  </div>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      x: 0
+    }
+  },
+  methods: {
+    handleMousedown (ev) {
+      let min = 0
+      let max = this.$refs.continue.offsetWidth
+      let startX = ev.clientX
+      let start = this.x
+
+      let mousemove = ev => {
+        let x = start + (ev.clientX - startX)
+        x = x < min ? min : (x > max ? max : x)
+        this.x = x
+
+        ev.stopPropagation()
+        ev.preventDefault()
+      }
+      let mouseup = ev => {
+        document.removeEventListener('mousemove', mousemove, false)
+        document.removeEventListener('mouseup', mouseup, false)
+      }
+
+      document.addEventListener('mousemove', mousemove, false)
+      document.addEventListener('mouseup', mouseup, false)
+
+      ev.stopPropagation()
+      ev.preventDefault()
+    }
+  },
+  computed: {
+    left () {
+      return this.x - 5 + 'px'
+    }
+  }
+}
+</script>
+
+<style scoped>
+@import url('./style.css');
+</style>

+ 50 - 0
src/components/dateRange/style.css

@@ -0,0 +1,50 @@
+.range-layout {
+  --height: 14px;
+  display: flex;
+  padding: 5px 15px;
+  background: linear-gradient(to right, #013B7D, #0C7AD8, #013B7D);
+  border-radius: calc(var(--height) / 2 + 5px);
+}
+
+.range-layout > span {
+  flex: 0 0 70px;
+  height: var(--height);
+  font-size: 12px;
+  color: #fff;
+  line-height: var(--height);
+}
+
+.continue {
+  position: relative;
+  flex: 1;
+  background-color: rgb(45,60,80);
+  margin: 2px 10px;
+  box-shadow:0 -5px 5px #656565 inset; 
+}
+
+.continue > a {
+  width: calc(var(--height) + 2px);
+  height: calc(var(--height) + 2px);
+  position: absolute;
+  left: 2px;
+  top: -3px;
+  background-color: rgb(10, 180, 250);
+  border-radius: 50%;
+  cursor: pointer;
+}
+
+.continue > a::after {
+  content: '';
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  width: 50%;
+  height: 50%;
+  transform: translateX(-50%) translateY(-50%);
+  background-color: rgb(0,100,160);
+  border-radius: 50%;
+  box-shadow:
+    -5px -5px 5px #656565 inset,
+    5px 5px 5px #656565 inset
+}
+

+ 66 - 0
src/components/icons/index.vue

@@ -0,0 +1,66 @@
+<template>
+  <i :style="{
+    backgroundImage: 'url('+icons+')',
+    backgroundPosition: left+'px '+top+'px'
+  }"> </i>
+</template>
+
+<script>
+import icons from '@/assets/images/icons.png'
+
+const position = {
+  cancel: {
+    top: -9,
+    left: 0
+  },
+  home: {
+    top: -43,
+    left: 0
+  },
+  homeactive: {
+    top: -78,
+    left: 0
+  },
+  case: {
+    top: -246,
+    left: 0
+  },
+  caseactive: {
+    top: -272,
+    left: 0
+  },
+  news: {
+    top: -299,
+    left: 0
+  },
+  newsactive: {
+    top: -323,
+    left: 0
+  }
+}
+
+export default {
+  props: ['name'],
+  data () {
+    return {icons}
+  },
+  computed: {
+    top () {
+      return position[this.name].top || 0
+    },
+    left () {
+      return position[this.name].left || 0
+    }
+  }
+}
+</script>
+
+<style scoped>
+  i {
+    display: inline-block;
+    background-repeat: no-repeat;
+    width: 14px;
+    height: 16px;
+    vertical-align: middle
+  }
+</style>

+ 207 - 0
src/data/index.js

@@ -0,0 +1,207 @@
+const data = {
+  detils: {
+    list: [
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      },
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      },
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      },
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      },
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      },
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      },
+      {
+        id: 1,
+        date: '2018-07-06 14:23:52',
+        type: '充值',
+        fled1: '2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '成功',
+        fled6: '20180705092731vPkAS0wn',
+        fled7: '30'
+      }
+    ],
+    statistics: {
+      filed1: '10000.00',
+      filed2: '12000.00',
+      filed3: '-10000.00',
+      filed4: '3000.00'
+    },
+    pag: {
+      total: 400,
+      current: 2,
+      size: 10
+    }
+  },
+  users: {
+    list: [
+      {
+        id: 'bof1ec603fa6',
+        init: '3399',
+        fled1: '-2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '-1000',
+        fled6: '300.00'
+      },
+      {
+        id: 'bof1ec603fa6',
+        init: '3399',
+        fled1: '-2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '-1000',
+        fled6: '300.00'
+      },
+      {
+        id: 'bof1ec603fa6',
+        init: '3399',
+        fled1: '-2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '-1000',
+        fled6: '300.00'
+      },
+      {
+        id: 'bof1ec603fa6',
+        init: '3399',
+        fled1: '-2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '-1000',
+        fled6: '300.00'
+      },
+      {
+        id: 'bof1ec603fa6',
+        init: '3399',
+        fled1: '-2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '-1000',
+        fled6: '300.00'
+      },
+      {
+        id: 'bof1ec603fa6',
+        init: '3399',
+        fled1: '-2000',
+        fled2: '1399',
+        fled3: '1000.00',
+        fled4: '1200',
+        fled5: '-1000',
+        fled6: '300.00'
+      }
+    ],
+    statistics: {
+      filed1: '10000.00',
+      filed2: '12000.00',
+      filed3: '-10000.00',
+      filed4: '3000.00'
+    },
+    pag: {
+      total: 400,
+      current: 2,
+      size: 10
+    }
+  }
+}
+
+export default {
+  getUsers () {
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve(data.users)
+      }, 1000)
+    })
+  },
+  getUserStatis () {
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve(data.users.statistics)
+      }, 1000)
+    })
+  },
+  getDetils () {
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve(data.detils)
+      }, 1000)
+    })
+  },
+  gettDetilStatis () {
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve(data.detils.statistics)
+      }, 1000)
+    })
+  }
+}

+ 27 - 0
src/main.js

@@ -0,0 +1,27 @@
+// 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 ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+import { configure } from '@/util/http'
+import prompt from '@/util/prompt'
+import wangEditor from 'wangeditor'
+import '@/util/extend'
+
+Vue.use(ElementUI)
+configure(Vue, router, ElementUI)
+prompt(Vue, ElementUI)
+Vue.config.productionTip = false
+Vue.prototype.Editor = wangEditor
+
+export default new Vue({
+  el: '#app',
+  router,
+  components: { App },
+  data: {
+    Bus: new Vue()
+  },
+  template: '<App/>'
+})

+ 247 - 0
src/pages/case/details/index.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="view">
+    <el-form label-width="80px" class="l-form">
+      <el-form-item label="封面图片">
+        <el-upload
+          class="cover-uploader"
+          :action="root + '/system/upload'"
+          :show-file-list="false"
+          :on-success="handleCoverSuccess"
+          :before-upload="handleCoverBefore"
+          :style="{width: '100%', paddingTop: '60%'}">
+          <div class="cover-content">
+            <img v-if="coverUrl" :src="coverUrl" class="cover">
+            <template v-else class="cover-content">
+              <i class="el-icon-plus cover-uploader-icon"></i>
+              <p>点击上传案例封面图片(限5M)<br> 支持格式为jpg/png</p>
+            </template>
+          </div>
+        </el-upload>
+      </el-form-item>
+
+      <!-- <el-form-item label="来源logo">
+        <el-upload
+          class="cover-uploader"
+          :action="root + '/system/upload'"
+          :show-file-list="false"
+          :on-success="handleLogoSuccess"
+          :before-upload="handleLogoBefore"
+          :style="{width: '50%', paddingTop: '50%'}">
+          <div class="cover-content">
+            <img v-if="logoUrl" :src="logoUrl" class="cover">
+            <template v-else class="cover-content">
+              <i class="el-icon-plus cover-uploader-icon"></i>
+              <p>点击上传LOGO(限5M)<br> 支持格式为jpg/png</p>
+            </template>
+          </div>
+        </el-upload>
+      </el-form-item> -->
+      <el-form-item label="标签">
+        <!-- <el-input v-model="label" placeholder="输入案例标签"></el-input> -->
+        <el-select v-model="label" placeholder="请选择标签">
+          <el-option label="文博" value="文博"></el-option>
+          <el-option label="工业" value="工业"></el-option>
+          <el-option label="地产" value="地产"></el-option>
+          <el-option label="政务" value="政务"></el-option>
+          <el-option label="文物" value="文物"></el-option>
+          <el-option label="电商" value="电商"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="排序">
+        <el-input v-model="sort" placeholder="输入序号,升序"></el-input>
+      </el-form-item>
+      <el-form-item label="地址">
+        <el-input v-model="link" placeholder="大场景链接" class="input-with-select">
+          <el-select v-model="type" style="width: 100px" slot="prepend" placeholder="请选择">
+            <el-option label="场景" :value="1"></el-option>
+            <el-option label="视频" :value="2"></el-option>
+          </el-select>
+        </el-input>
+      </el-form-item>
+      <el-form-item label="语言">
+        <el-input
+          v-model="on_language"
+          :placeholder="
+            !language ? '请选择当前文章的语言':
+              language === 1 ? '请填写对应英文文章ID' : '请填写对应中文文章ID'
+          "
+          class="input-with-select">
+          <el-select v-model="language" style="width: 100px" slot="prepend" placeholder="请选择">
+            <el-option label="中文" :value="1"></el-option>
+            <el-option label="英文" :value="2"></el-option>
+            <el-option label="德文" :value="3"></el-option>
+          </el-select>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item label="项目类型">
+        <!-- <el-input v-model="label" placeholder="输入案例标签"></el-input> -->
+
+        <el-select v-model="proType" placeholder="请选择项目类型">
+          <el-option label="不限" :value="0"></el-option>
+          <el-option label="PC" :value="1"></el-option>
+          <el-option label="手机端" :value="2"></el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- <el-form-item label="项目地点">
+        <el-input v-model="address" placeholder="输入项目地点"></el-input>
+      </el-form-item> -->
+      <!-- <el-form-item label="编辑时间">
+        <el-date-picker
+          v-model="adate"
+          align="right"
+          type="date"
+          placeholder="选择时间"
+          style="width: 100%">
+        </el-date-picker>
+      </el-form-item> -->
+    </el-form>
+    <el-form label-width="80px" class="r-form">
+      <!-- <el-form-item label="独占一列">
+        <el-select v-model="type" placeholder="请选择">
+          <el-option label="是" :value="2"></el-option>
+          <el-option label="否" :value="1"></el-option>
+        </el-select>
+      </el-form-item> -->
+
+      <el-form-item label="案例标题">
+        <el-input v-model="title" placeholder="输入案例标题,限30字"></el-input>
+      </el-form-item>
+      <el-form-item label="正文内容">
+        <div ref="editContent">
+        </div>
+      </el-form-item>
+      <el-form-item>
+        <div style="float: right">
+          <el-button type="info" plain @click="$router.back()">取消</el-button>
+          <el-button type="primary" @click="submit">{{id ? '修改' : '新增'}}</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import {root} from '@/util/http'
+
+export default {
+  data () {
+    return {
+      id: null,
+      coverUrl: '',
+      logoUrl: '',
+      label: '',
+      sort: '',
+      link: '',
+      address: '',
+      adate: '',
+      title: '',
+      content: '',
+      type: 1,
+      proType: 0,
+      language: null,
+      on_language: null,
+      root
+    }
+  },
+  methods: {
+    handleCoverBefore () {
+    },
+    handleLogoBefore () {
+    },
+    handleCoverSuccess (data) {
+      this.coverUrl = data.content
+    },
+    handleLogoSuccess (data) {
+      this.logoUrl = data.content
+    },
+    async submit () {
+      if (!this.title) {
+        return this.$error('请填写标题')
+      } else if (!this.coverUrl) {
+        return this.$error('请上传封面')
+      }
+      let body = {
+        proType: this.proType,
+        language: this.language,
+        title: this.title,
+        content: this.content,
+        cover: this.coverUrl,
+        logo: this.logoUrl,
+        label: this.label,
+        date: new Date(this.adate).getTime(),
+        sort: this.sort,
+        link: this.link,
+        type: this.type,
+        address: this.address
+      }
+
+      if (body.language === 1) {
+        body.english_id = this.on_language
+      } else {
+        body.chinese_id = this.on_language
+      }
+
+      try {
+        if (this.id) {
+          body.id = this.id
+          await this.$http.put('/case', body)
+          this.$success('修改成功')
+        } else {
+          await this.$http.post('/case', body)
+          this.$router.back()
+        }
+      } catch (e) {
+        this.$error(e.msg)
+      }
+    },
+    async getInfo () {
+      try {
+        let {content} = await this.$http.get('/case/' + this.id)
+        this.proType = content.pro_type
+        this.title = content.title
+        this.content = content.content
+        this.coverUrl = content.cover
+        this.logoUrl = content.logo
+        this.label = content.label
+        this.adate = new Date(Number(content.date))
+        this.sort = content.sort
+        this.link = content.link
+        this.type = content.type
+        this.address = content.address
+        this.language = content.language || null
+
+        if (content.language === 1) {
+          this.on_language = content.english_id || null
+        } else {
+          this.on_language = content.chinese_id || null
+        }
+      } catch (e) {
+        this.$router.back()
+      }
+    }
+  },
+  async mounted () {
+    let editor = new this.Editor(this.$refs.editContent)
+    editor.customConfig.onchange = html => {
+      this.content = html
+    }
+    editor.customConfig.uploadImgServer = root + '/system/uploads'
+    editor.customConfig.uploadFileName = 'file'
+    editor.customConfig.uploadImgMaxLength = 1
+    editor.create()
+
+    let id = Number(this.$route.params.id)
+    if (Number.isFinite(id)) {
+      this.id = id
+      await this.getInfo()
+      editor.txt.html(this.content)
+    }
+  }
+}
+</script>
+
+<style scoped>
+@import url('./style.css');
+</style>

+ 18 - 0
src/pages/case/details/style.css

@@ -0,0 +1,18 @@
+.view {
+  background-color: #fff;
+  padding: 20px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+  display: flex
+}
+
+.l-form {
+  flex: 0 0 450px;
+}
+
+.r-form {
+  flex: 1 0 0;
+  padding-left: 10px;
+  border-left: 1px solid #dcdfe6;
+  margin-left: 40px;
+}

+ 118 - 0
src/pages/case/list/index.vue

@@ -0,0 +1,118 @@
+<template>
+  <div>
+    <el-form ref="form" label-width="80px" :inline="true" class="form screen">
+      <el-form-item label="关键词:" class="el-form-item">
+        <el-input v-model="keyword"></el-input>
+      </el-form-item>
+      <el-button
+        style="float: right"
+        type="primary"
+        @click="$router.push({name: 'caseDetails', params: {id: 'add'}})">
+        添加
+      </el-button>
+    </el-form>
+    <div class="view">
+      <el-table
+        v-loading="loading"
+        :data="cases"
+        style="width: 100%"
+        class="creamtable table"
+        :highlight-current-row="false"
+        >
+        <el-table-column label="id" prop="id"></el-table-column>
+        <el-table-column label="封面图片" width="290">
+          <img slot-scope="{row: {cover}}" :src="cover" style="max-width: 100%">
+        </el-table-column>
+        <el-table-column label="案例标题" prop="title"></el-table-column>
+        <el-table-column label="标签" prop="label"></el-table-column>
+        <el-table-column label="时间" prop="date">
+          <p slot-scope="{row: {date}}">{{new Date(Number(date)).format('yyyy-MM-dd')}}</p>
+        </el-table-column>
+
+        <el-table-column label="操作" width="160">
+          <template slot-scope="{row: {id}}">
+            <router-link :to="{name: 'caseDetails', params: {id}}" class="link edit">修改</router-link>
+            <a @click="removeItem(id)" class="link delete">删除</a>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pag-layout" v-if="pag.total">
+        <el-pagination
+          :total="pag.total"
+          :current-page="pag.page"
+          :page-size="pag.limit"
+          @current-change="handleCurrentChange"
+          layout="total, prev, pager, next, jumper"
+          class="pag">
+        </el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  props: ['screen'],
+  data () {
+    return {
+      keyword: '',
+      cases: [],
+      pag: {
+        page: 1,
+        limit: 10
+      },
+      loading: true
+    }
+  },
+  methods: {
+    async getData () {
+      let params = {
+        ...this.pag,
+        keyword: this.keyword
+      }
+      --params.page
+      let result = await this.$http.get('/case', {params: params})
+      this.cases = result.content.data
+      this.pag.total = result.content.total
+      this.pag.limit = result.content.average
+    },
+    async removeItem (id) {
+      if (confirm('删除后将无法恢复,您确定要删除此数据吗?')) {
+        try {
+          await this.$http.delete('/case/' + id)
+          this.$message('成功删除')
+          this.refresh()
+        } catch (e) {
+          this.$error(e.msg)
+        }
+      }
+    },
+    async refresh () {
+      this.loading = true
+      await this.getData()
+      this.loading = false
+    },
+    handleCurrentChange (page) {
+      this.pag.page = page
+      this.refresh()
+    }
+  },
+  watch: {
+    keyword () {
+      this.refresh()
+    }
+  },
+  activated () {
+    this.refresh()
+  },
+  mounted () {
+    this.refresh()
+  }
+}
+</script>
+
+<style scoped>
+@import url('./style.css');
+</style>

+ 49 - 0
src/pages/case/list/style.css

@@ -0,0 +1,49 @@
+
+.screen {
+  background-color: #fff;
+  margin-bottom: 30px;
+  padding: 20px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+}
+
+.el-form-item {
+  margin-bottom: 0;
+}
+
+.btn {
+  color: #fff;
+  background-color: #09aafe;
+  border: none;
+}
+
+.view {
+  background-color: #fff;
+  padding: 20px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+}
+
+
+
+.pag-layout {
+  padding-top: 20px;
+  overflow: hidden;
+}
+
+.pag {
+  float: right;
+}
+
+.link {
+  font-size: 14px;
+}
+
+.edit {
+  color: #09aafe;
+}
+
+.delete {
+  color: #f56c6c;
+  margin-left: 15px;
+}

+ 63 - 0
src/pages/home/index.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <div class="model m1">
+      <p>欢迎使用四维看看后台代理系统!</p>
+      <p>四维时代致力于让数字化飞入寻常百姓家</p>
+      <p>让人工智能真正服务于生活</p>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.model {
+  background-color: #fff;
+  margin-bottom: 30px;
+  padding: 10px 18px;
+  box-shadow: 0 0 10px rgba(0,0,0,0.1);
+  border-radius: 3px;
+}
+
+.m1 {
+  font-size: 16px;
+  color: #333333;
+  line-height: 30px;
+  padding-top: 15px;
+  padding-bottom: 15px;
+}
+
+.m1 p:first-child {
+  color: #999999;
+  font-size: 14px;
+  font-weight: bold;
+  font-style: italic;
+}
+
+.m2 h4{
+  font-size: 14px;
+  color: #333333;
+  line-height: 40px;
+  font-weight: bold;
+  padding-bottom: 5px;
+  border-bottom: 1px solid #f2f2f2;
+}
+
+.m2 div {
+  color: #333333;
+  font-size: 16px;
+  line-height: 30px;
+  padding-top: 15px;
+  padding-bottom: 30px;
+  /* max-width: 980px; */
+  /* margin: 0 auto */
+}
+
+.m2 h5 {
+  font-weight: bold;
+  margin: 10px 0 20px;
+}
+
+.m2 * {
+  word-wrap: break-word;
+  word-break: break-all;
+}
+</style>

+ 151 - 0
src/pages/layout/index.vue

@@ -0,0 +1,151 @@
+<template>
+  <el-container class="container">
+    <el-header class="header">
+      <img src="@/assets/images/logo.png" alt="">
+      <a @click="quit" class="span">
+        <icon name="cancel" class="icon" />注销
+      </a>
+    </el-header>
+    <el-container>
+      <el-aside width="240px" class="aside">
+        <p>导航面板</p>
+        <div class="nav">
+          <router-link :to="{name: nav.name}" v-for="nav in navs" :key="nav.name" :class="'p'+ (nav.active ? ' active': '')" >
+            <icon :name="nav.icon + (nav.active ? 'active': '')" class="i" />{{nav.text}}
+          </router-link>
+        </div>
+      </el-aside>
+      <el-main class="main">
+        <div class="nav-head">
+          <h4>
+            {{info.title}}
+            <p v-for="(param, i) in tools" :key="i">
+              <span :class="param.class">
+                <i>{{param.key}}</i>
+                <b>{{param.value}}</b>
+              </span>
+            </p>
+          </h4>
+          <div>
+            <router-link v-for="path in paths" :key="path.name" :to="{name: path.name}" class="link" v-if="path.remarks">
+              {{path.remarks}}
+            </router-link>
+          </div>
+        </div>
+        <keep-alive>
+          <router-view class="show-view" v-if="$route.meta.cache" />
+        </keep-alive>
+        <router-view class="show-view" v-if="!$route.meta.cache" />
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import icon from '@/components/icons'
+
+function queryPath (router, name) {
+  let paths = []
+  let result = null
+
+  for (let i = 0; i < router.length; i++) {
+    if (router[i].children) {
+      result = queryPath(router[i].children, name)
+    }
+    if (result) {
+      paths.push(router[i])
+      break
+    } else if (router[i].name === name) {
+      result = [router[i]]
+      break
+    }
+  }
+  return paths.concat(result || [])
+}
+
+export default {
+  data () {
+    return {
+      router: this.$router.options.routes,
+      paths: [],
+      tools: [],
+      navs: [
+        {
+          text: '首页',
+          name: 'home',
+          icon: 'home',
+          active: false
+        },
+        {
+          text: '客户案例',
+          name: 'caseList',
+          icon: 'case',
+          active: false
+        },
+        {
+          text: '新闻报道',
+          name: 'newsList',
+          icon: 'news',
+          active: false
+        }
+      ]
+    }
+  },
+  computed: {
+    info () {
+      let current = null
+      for (let i = this.paths.length - 1; i >= 0; i--) {
+        current = this.paths[i]
+        if (current.remarks) break
+      }
+
+      return {
+        title: current && current.remarks,
+        name: current && current.name
+      }
+    }
+  },
+  watch: {
+    info: {
+      deep: true,
+      handler () {
+        this.navs.forEach(nav => {
+          if (~this.paths.findIndex(path => path.name === nav.name)) {
+            nav.active = true
+          } else {
+            nav.active = false
+          }
+        })
+      }
+    }
+  },
+  methods: {
+    async quit () {
+      let result = await this.$http.post('/user/sinout')
+      if (result) {
+        this.$router.push({name: 'login'})
+        sessionStorage.removeItem('name')
+      }
+    }
+  },
+  created () {
+    this.paths = queryPath(this.router, this.$router.history.current.name)
+    this.$root.Bus.$on('addTool', data => {
+      this.tools.push(data)
+    })
+    this.$root.Bus.$on('rmTool', data => {
+      let index = this.tools.indexOf(data)
+      ~index && this.tools.splice(index, 1)
+    })
+  },
+  beforeRouteUpdate (to, from, next) {
+    this.paths = queryPath(this.router, to.name)
+    next()
+  },
+  components: {icon}
+}
+</script>
+
+<style scoped>
+  @import url('./style.css');
+</style>

+ 152 - 0
src/pages/layout/style.css

@@ -0,0 +1,152 @@
+.container {
+  background-color: #f2f2f2;
+  height: 100%;
+}
+
+.header {
+  background: #333333;
+  font-size: 18px;
+  overflow: hidden;
+  height: 68px !important;
+  position: relative;
+}
+
+.header img {
+  height: 28px;
+  top: 50%;
+  left: 25px;
+  position: absolute;
+  transform: translateY(-50%);
+}
+
+.header .span {
+  float: right;
+  color: #fff;
+  font-size: 14px;
+  top: 50%;
+  right: 25px;
+  position: absolute;
+  transform: translateY(-50%);
+  vertical-align: middle
+}
+
+.header .span .icon {
+  margin-right: 5px;
+}
+
+.aside {
+  background-color: #fff;
+  box-shadow: 5px 5px 10px rgba(0,0,0,0.1);
+  position: relative;
+  z-index: 2
+}
+
+.aside > p {
+  padding-left: 20px;
+  line-height: 60px;
+  font-size: 12px;
+  color: #999999;
+}
+
+.nav .p {
+  height: 58px;
+  line-height: 58px;
+  font-size: 14px;
+  color: #999999;
+  cursor: pointer;
+  padding-left: 45px;
+  padding-right: 20px;
+  position: relative;
+  display: block;
+}
+
+.nav .p .i {
+  position: absolute;
+  left: 20px;
+  top: 50%;
+  margin-top: -9px;
+}
+
+.nav .p.active {
+  background-color: #f2f2f2;
+  color: #333333;
+}
+
+.main {
+  padding: 0;
+  position: relative;
+  z-index: 1;
+}
+
+.nav-head {
+  padding: 0 25px;
+  height: 60px;
+  background-color: #fff;
+  position: relative;
+  box-shadow: 5px 0px 10px rgba(0, 0, 0, 0.1)
+}
+
+.main h4 {
+  font-size: 18px;
+  line-height: 60px;
+  font-weight: 400;
+}
+
+.main h4 span {
+  padding-left: 15px;
+}
+
+.main h4 p {
+  display: inline-block;
+}
+
+.main h4 p span i::after {
+  content: ':'
+}
+
+.main h4 p span b {
+  font-weight: normal;
+}
+
+.main h4 p span.strong i {
+  font-size: 14px;
+  color: #f56c6c;
+}
+
+.main h4 p span.strong b {
+  font-size: 18px;
+  color: #f56c6c;
+}
+
+.main h4 p span.strong b::after {
+  content: '点';
+  font-size: 14px;
+}
+
+.nav-head div {
+  position: absolute;
+  height: 16px;
+  top: 50%;
+  right: 25px;
+  margin-top: -8px;
+  color: #999999;
+}
+
+.nav-head .link {
+  color: inherit;
+  line-height: 16px;
+  font-size: 14px;
+}
+
+.nav-head .link::after {
+  content: '>';
+  padding: 0 5px;
+}
+
+.nav-head .link:last-child::after {
+  display: none;
+}
+
+.show-view {
+  margin: 30px;
+}

+ 102 - 0
src/pages/login/index.css

@@ -0,0 +1,102 @@
+.title {
+  font-size: 24px;
+  color: #333333;
+  padding: 50px 0;
+  text-align: center;
+}
+
+.login {
+  height: 600px;
+  background: no-repeat center center;
+  background-size: cover;
+}
+
+.content {
+  max-width: 1820px;
+  margin: 0 auto;
+  position: relative;
+  height: 100%;
+}
+
+.text {
+  position: absolute;
+  color: #fff;
+  top: 115px;
+  left: 160px;
+}
+
+.text img {
+  padding-bottom: 60px;
+}
+
+.text h1 {
+  font-size: 60px;
+  padding-bottom: 25px;
+}
+
+.text h2 {
+  font-size: 40px;
+  font-weight: 300;
+  padding-bottom: 25px;
+}
+
+.text p {
+  font-size: 20px;
+}
+
+.from {
+  position: absolute;
+  right: 90px;
+  width: 340px;
+  background-color: #ffffff;
+  padding: 15px 30px 40px;
+  box-sizing: border-box;
+  top: 50%;
+  transform: translateY(-50%);
+  box-shadow: 5px 5px 5px rgba(0,0,0,0.2)
+}
+
+.from h3 {
+  font-size: 16px;
+  color: #333333;
+  line-height: 45px;
+  border-bottom: 1px solid #27b5ff;
+  text-align: center;
+  font-weight: 400;
+  margin-bottom: 40px;
+}
+
+.button {
+  width: 100%;
+  background-color: #09aafe;
+  color: #fff;
+}
+
+.footer {
+  background-color: #f5f5f5;
+  padding: 20px 0;
+}
+
+.footer p {
+  text-align: center;
+  font-size: 12px;
+  color: #999999;
+  line-height: 30px;
+}
+
+.footer span {
+  font-size: 14px;
+  color: #333333;
+  display: inline-block;
+  padding-bottom: 10px;
+}
+
+.footer span::after {
+  content: '|';
+  display: inline-block;
+  padding: 0 12px;
+}
+
+.footer span:last-child::after {
+  display: none;
+}

+ 94 - 0
src/pages/login/index.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="layout">
+    <h2 class="title">四维时代 · 管理后台</h2>
+    <div class="login" :style="{backgroundImage: 'url('+bg+')'}">
+      <div class="content">
+        <div class="text">
+          <img src="@/assets/images/logo.png" alt="">
+          <h1>致力于<br>让数字化飞入寻常百姓家</h1>
+          <h2>让人工智能真正服务于生活</h2>
+        </div>
+        <el-form class="from" v-loading="loadding">
+          <h3>代理登录</h3>
+          <el-form-item>
+            <el-input v-model="name" placeholder="账号"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-input v-model="password" type="password" placeholder="密码"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-checkbox-group v-model="remember">
+              <el-checkbox label="记住密码(在公共场所电脑请勿勾选)" name="type"></el-checkbox>
+            </el-checkbox-group>
+          </el-form-item>
+          <el-form-item>
+            <el-button class="button" @click="onSubmit">登陆</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+    <div class="footer">
+      <p>TEL: 4006698025  |  FAX: 0756-6996790  |  E-Mail:sales@4dage.com</p>
+      <p>粤ICP备14078495号-3</p>
+      <p>Copyright2018 Sitename. Design by 4Dage Technology Co.Ltd.</p>
+    </div>
+  </div>
+</template>
+
+<script>
+import cookie from '@/util/cookie'
+import bg from '@/assets/images/bg.jpg'
+
+export default {
+  name: 'login',
+  data () {
+    return {
+      loadding: false,
+      name: '',
+      password: '',
+      remember: false,
+      bg
+    }
+  },
+  methods: {
+    async onSubmit () {
+      if (this.name.trim().length < 4) {
+        return this.$error('用户名格式不正确!')
+      } else if (this.password.length < 4) {
+        return this.$error('密码格式不正确!')
+      }
+      this.loadding = true
+
+      try {
+        await this.$http.post('/user/sign', {
+          name: this.name,
+          password: this.password
+        })
+
+        sessionStorage.setItem('name', this.name)
+        this.$router.push({name: 'home'})
+        if (this.remember) {
+          cookie.setCookie('name', this.name)
+          cookie.setCookie('password', this.password)
+        } else {
+          cookie.setCookie('name', '')
+          cookie.setCookie('password', '')
+        }
+      } catch (e) {
+        this.$error(e.msg)
+      }
+
+      this.loadding = false
+    }
+  },
+  created () {
+    this.name = cookie.getCookie('name')
+    this.password = cookie.getCookie('password')
+    this.remember = !!this.name
+  }
+}
+</script>
+
+<style scoped>
+  @import './index.css'
+</style>

+ 193 - 0
src/pages/news/details/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="view">
+    <el-form label-width="80px" class="l-form">
+      <el-form-item label="封面图片">
+        <el-upload
+          class="cover-uploader"
+          :action="root + '/system/upload'"
+          :show-file-list="false"
+          :on-success="handleCoverSuccess"
+          :before-upload="handleCoverBefore"
+          :style="{width: '100%', paddingTop: '60%'}">
+          <div class="cover-content">
+            <img v-if="coverUrl" :src="coverUrl" class="cover">
+            <template v-else class="cover-content">
+              <i class="el-icon-plus cover-uploader-icon"></i>
+              <p>点击上传案例封面图片(限5M)<br> 支持格式为jpg/png</p>
+            </template>
+          </div>
+        </el-upload>
+      </el-form-item>
+
+      <el-form-item label="标签">
+        <el-input v-model="label" placeholder="输入案例标签"></el-input>
+      </el-form-item>
+      <el-form-item label="排序">
+        <el-input v-model="sort" placeholder="输入序号,升序"></el-input>
+      </el-form-item>
+      <el-form-item label="编辑时间">
+        <el-date-picker
+          v-model="adate"
+          align="right"
+          type="date"
+          placeholder="选择时间"
+          style="width: 100%">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="来源">
+        <el-input v-model="origin" placeholder="输入新闻来源,限30字"></el-input>
+      </el-form-item>
+      <el-form-item label="语言">
+
+        <el-input
+          v-model="on_language"
+          :placeholder="
+            !language ? '请选择当前文章的语言':
+              language === 1 ? '请填写对应英文文章ID' : '请填写对应中文文章ID'
+          "
+          class="input-with-select">
+          <el-select v-model="language" style="width: 100px" slot="prepend" placeholder="请选择">
+            <el-option label="中文" :value="1"></el-option>
+            <el-option label="英文" :value="2"></el-option>
+            <el-option label="德文" :value="3"></el-option>
+          </el-select>
+        </el-input>
+
+      </el-form-item>
+      <el-form-item label="转向中文">
+        <el-select v-model="redire" style="width: 100px">
+          <el-option label="否" :value="0">否</el-option>
+          <el-option label="是" :value="1">是</el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+
+    <el-form label-width="80px" class="r-form">
+      <el-form-item label="案例标题">
+        <el-input v-model="title" placeholder="输入案例标题,限30字"></el-input>
+      </el-form-item>
+      <el-form-item label="正文内容">
+        <div ref="editContent">
+        </div>
+      </el-form-item>
+      <el-form-item>
+        <div style="float: right">
+          <el-button type="info" plain @click="$router.back()">取消</el-button>
+          <el-button type="primary" @click="submit">{{id ? '修改' : '新增'}}</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import {root} from '@/util/http'
+export default {
+  data () {
+    return {
+      id: null,
+      coverUrl: '',
+      label: '',
+      sort: '',
+      adate: '',
+      title: '',
+      content: '',
+      origin: '',
+      language: null,
+      on_language: null,
+      redire: 1,
+      root
+    }
+  },
+  methods: {
+    handleCoverBefore () {
+    },
+    handleCoverSuccess (data) {
+      this.coverUrl = data.content
+    },
+    async submit () {
+      if (!this.title) {
+        return this.$error('请填写标题')
+      } else if (!this.adate) {
+        return this.$error('请选择日期')
+      } else if (!this.coverUrl) {
+        return this.$error('请上传封面')
+      }
+
+      let body = {
+        language: this.language,
+        title: this.title,
+        content: this.content,
+        cover: this.coverUrl,
+        label: this.label,
+        date: new Date(this.adate).getTime(),
+        sort: this.sort,
+        origin: this.origin,
+        redire: this.redire
+      }
+
+      if (body.language === 1) {
+        body.english_id = this.on_language
+      } else {
+        body.chinese_id = this.on_language
+      }
+
+      try {
+        if (this.id) {
+          body.id = this.id
+          await this.$http.put('/news', body)
+          this.$success('修改成功')
+        } else {
+          await this.$http.post('/news', body)
+          this.$router.back()
+        }
+      } catch (e) {
+        this.$error(e.msg)
+      }
+    },
+    async getInfo () {
+      try {
+        let {content} = await this.$http.get('/news/' + this.id)
+        this.title = content.title
+        this.content = content.content
+        this.coverUrl = content.cover
+        this.label = content.label
+        this.adate = new Date(Number(content.date))
+        this.sort = content.sort
+        this.origin = content.origin
+        this.redire = content.redire
+        this.language = content.language || null
+
+        if (content.language === 1) {
+          this.on_language = content.english_id || null
+        } else {
+          this.on_language = content.chinese_id || null
+        }
+      } catch (e) {
+        this.$router.back()
+      }
+    }
+  },
+  async mounted () {
+    let editor = new this.Editor(this.$refs.editContent)
+    editor.customConfig.onchange = html => {
+      this.content = html
+    }
+    editor.customConfig.uploadImgServer = root + '/system/uploads'
+    editor.customConfig.uploadFileName = 'file'
+    editor.customConfig.uploadImgMaxLength = 1
+    editor.create()
+
+    let id = Number(this.$route.params.id)
+    if (Number.isFinite(id)) {
+      this.id = id
+      await this.getInfo()
+      editor.txt.html(this.content)
+    }
+  }
+}
+</script>
+
+<style scoped>
+@import url('./style.css');
+</style>

+ 18 - 0
src/pages/news/details/style.css

@@ -0,0 +1,18 @@
+.view {
+  background-color: #fff;
+  padding: 20px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+  display: flex
+}
+
+.l-form {
+  flex: 0 0 450px;
+}
+
+.r-form {
+  flex: 1 0 0;
+  padding-left: 10px;
+  border-left: 1px solid #dcdfe6;
+  margin-left: 40px;
+}

+ 119 - 0
src/pages/news/list/index.vue

@@ -0,0 +1,119 @@
+<template>
+  <div>
+    <el-form ref="form" label-width="80px" :inline="true" class="form screen">
+      <el-form-item label="关键词:" class="el-form-item">
+        <el-input v-model="keyword"></el-input>
+      </el-form-item>
+      <el-button
+        style="float: right"
+        type="primary"
+        @click="$router.push({name: 'newsDetails', params: {id: 'add'}})">
+        添加
+      </el-button>
+    </el-form>
+    <div class="view">
+      <el-table
+        v-loading="loading"
+        :data="news"
+        style="width: 100%"
+        class="creamtable table"
+        :highlight-current-row="false"
+        >
+        <el-table-column label="id" prop="id"></el-table-column>
+        <el-table-column label="封面图片" width="290">
+          <img slot-scope="{row: {cover}}" :src="cover" style="max-width: 100%">
+        </el-table-column>
+        <el-table-column label="新闻标题" prop="title"></el-table-column>
+        <el-table-column label="标签" prop="label"></el-table-column>
+        <el-table-column label="更新时间" prop="date">
+          <p slot-scope="{row: {date}}">{{new Date(Number(date)).format('yyyy-MM-dd')}}</p>
+        </el-table-column>
+        <el-table-column label="排序" prop="sort"></el-table-column>
+
+        <el-table-column label="操作" width="160">
+          <template slot-scope="{row: {id}}">
+            <router-link :to="{name: 'newsDetails', params: {id}}" class="link edit">修改</router-link>
+            <a @click="removeItem(id)" class="link delete">删除</a>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pag-layout" v-if="pag.total">
+        <el-pagination
+          :total="pag.total"
+          :current-page="pag.page"
+          :page-size="pag.limit"
+          @current-change="handleCurrentChange"
+          layout="total, prev, pager, next, jumper"
+          class="pag">
+        </el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  props: ['screen'],
+  data () {
+    return {
+      keyword: '',
+      news: [],
+      pag: {
+        page: 1,
+        limit: 10
+      },
+      loading: true
+    }
+  },
+  methods: {
+    async getData () {
+      let params = {
+        ...this.pag,
+        keyword: this.keyword
+      }
+      --params.page
+      let result = await this.$http.get('/news', {params: params})
+      this.news = result.content.data
+      this.pag.total = result.content.total
+      this.pag.limit = result.content.average
+    },
+    async removeItem (id) {
+      if (confirm('删除后将无法恢复,您确定要删除此数据吗?')) {
+        try {
+          await this.$http.delete('/news/' + id)
+          this.$message('成功删除')
+          this.refresh()
+        } catch (e) {
+          this.$error(e.msg)
+        }
+      }
+    },
+    async refresh () {
+      this.loading = true
+      await this.getData()
+      this.loading = false
+    },
+    handleCurrentChange (page) {
+      this.pag.page = page
+      this.refresh()
+    }
+  },
+  watch: {
+    keyword () {
+      this.refresh()
+    }
+  },
+  activated () {
+    this.refresh()
+  },
+  mounted () {
+    this.refresh()
+  }
+}
+</script>
+
+<style scoped>
+@import url('./style.css');
+</style>

+ 49 - 0
src/pages/news/list/style.css

@@ -0,0 +1,49 @@
+
+.screen {
+  background-color: #fff;
+  margin-bottom: 30px;
+  padding: 20px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+}
+
+.el-form-item {
+  margin-bottom: 0;
+}
+
+.btn {
+  color: #fff;
+  background-color: #09aafe;
+  border: none;
+}
+
+.view {
+  background-color: #fff;
+  padding: 20px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+}
+
+
+
+.pag-layout {
+  padding-top: 20px;
+  overflow: hidden;
+}
+
+.pag {
+  float: right;
+}
+
+.link {
+  font-size: 14px;
+}
+
+.edit {
+  color: #09aafe;
+}
+
+.delete {
+  color: #f56c6c;
+  margin-left: 15px;
+}

+ 77 - 0
src/router/index.js

@@ -0,0 +1,77 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import Login from '@/pages/login'
+import Layout from '@/pages/layout'
+import Case from '@/pages/case/list'
+import CaseDetails from '@/pages/case/details'
+import News from '@/pages/news/list'
+import NewsDetails from '@/pages/news/details'
+import Home from '@/pages/home'
+
+Vue.use(Router)
+
+let router = new Router({
+  routes: [
+    {
+      path: '/login',
+      name: 'login',
+      component: Login
+    },
+    {
+      path: '/',
+      name: 'layout',
+      component: Layout,
+      remarks: '管理后台',
+      redirect: '/',
+      children: [
+        {
+          path: '',
+          name: 'home',
+          component: Home,
+          remarks: '首页',
+          meta: {cache: true}
+        },
+        {
+          path: 'case',
+          name: 'caseList',
+          remarks: '客户案例',
+          component: Case,
+          meta: { cache: true }
+        },
+        {
+          path: 'case/:id',
+          name: 'caseDetails',
+          component: CaseDetails,
+          remarks: '案例编辑'
+        },
+        {
+          path: 'news',
+          name: 'newsList',
+          remarks: '新闻报道',
+          component: News,
+          meta: { cache: true }
+        },
+        {
+          path: 'news/:id',
+          name: 'newsDetails',
+          component: NewsDetails,
+          remarks: '新闻编辑'
+        }
+      ]
+    }
+  ]
+})
+
+router.beforeEach((to, from, next) => {
+  if (to.path !== '/login' && !sessionStorage.getItem('name')) {
+    next({ path: '/login' })
+  } else {
+    next()
+  }
+})
+
+router.afterEach(() => {
+  window.scrollTo(0, 0)
+})
+
+export default router

+ 29 - 0
src/util/cookie.js

@@ -0,0 +1,29 @@
+let fns = {
+  // 设置cookie
+  setCookie: function (cName, value, expiremMinutes = 24 * 60) {
+    let exdate = new Date()
+    exdate.setTime(exdate.getTime() + expiremMinutes * 60 * 1000)
+    document.cookie = cName + '=' + escape(value) + ((expiremMinutes === null) ? '' : 'expires=' + exdate.toGMTString())
+  },
+
+  // 读取cookie
+  getCookie: function (cName) {
+    let value = ''
+    let str = document.cookie
+    let start = str.indexOf(cName + '=')
+    if (~start) {
+      str = str.substr(start)
+      let end = str.indexOf('expires')
+
+      if (~end) {
+        value = str.substring(cName.length + 1, end)
+      } else {
+        value = str.substr(cName.length + 1)
+      }
+    }
+
+    return value
+  }
+}
+
+export default fns

+ 34 - 0
src/util/extend.js

@@ -0,0 +1,34 @@
+function dateFtt (fmt) {
+  let date = this
+  let o = {
+    'M+': date.getMonth() + 1,
+    'd+': date.getDate(),
+    'h+': date.getHours(),
+    'm+': date.getMinutes(),
+    's+': date.getSeconds(),
+    'q+': Math.floor((date.getMonth() + 3) / 3),
+    'S': date.getMilliseconds()
+  }
+
+  if (/(y+)/.test(fmt)) {
+    fmt = fmt.replace(
+      RegExp.$1,
+      (date.getFullYear() + '').substr(4 - RegExp.$1.length)
+    )
+  }
+
+  for (let k in o) {
+    if (new RegExp('(' + k + ')').test(fmt)) {
+      fmt = fmt.replace(
+        RegExp.$1,
+        RegExp.$1.length === 1
+          ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
+      )
+    }
+  }
+  return fmt
+}
+
+/* eslint-disable */
+Date.prototype.format = dateFtt
+/* eslint-enable */

+ 88 - 0
src/util/http.js

@@ -0,0 +1,88 @@
+import axios from 'axios'
+import qs from 'qs'
+
+// const root = 'http://www.4dage.com/newOfficialapi/'
+const root = '/newOfficialapi/'
+
+// const root = 'http://localhost:7000/'
+// const root = 'http://www.4dage.com:7000'
+// 配置请求域名
+axios.defaults.baseURL = root
+// axios.defaults.baseURL = 'http://www.4dage.com/newOfficialapi/'
+
+const configure = (Vue, router, { Message }) => {
+  axios.interceptors.request.use(function (config) {
+    config.data = qs.stringify(config.data)
+    return config
+  }, function (error) {
+    // 对请求错误做些什么
+    return Promise.reject(error)
+  })
+
+  // 配置response拦截器
+  axios.interceptors.response.use(
+    response => {
+      let data = response.data
+      let code = Number(data)
+
+      switch (code) {
+        case -100:
+          break
+        case -10:
+          Message({
+            type: 'error',
+            message: '登陆状态失效,请重新登陆'
+          })
+          router.replace({
+            name: 'login'
+          })
+          break
+
+        case -101:
+          Message({
+            type: 'error',
+            message: '服务器异常,请稍后再试!'
+          })
+          break
+
+        case -102:
+          Message({
+            type: 'error',
+            message: '缺少必要参数!'
+          })
+          break
+      }
+      return data
+    },
+    error => {
+      if (error.response) {
+        switch (error.response.status) {
+          case 500:
+            Message({
+              customClass: 'system-err',
+              type: 'error',
+              message: '服务器错误!'
+            })
+            break
+        }
+      }
+      return Promise.reject(error.response.data)
+    }
+  )
+  // axios.upload = async function (file) {
+  //   let { data: {content: token}} = await this.get('/qiniuToken')
+  //   let param = new FormData()
+  //   param.append('file', file)
+  //   param.append('token', token)
+
+  //   let {data: {key}} = await this.post('http://upload-z2.qiniup.com', param, {
+  //     headers: { 'Content-Type': 'multipart/form-data' }
+  //   })
+
+  //   return 'http://video.cgaii.com/' + key
+  // }
+  Vue.prototype.$http = axios
+}
+
+export default axios
+export { configure, root }

+ 15 - 0
src/util/prompt.js

@@ -0,0 +1,15 @@
+export default function (Vue, {Message}) {
+  Vue.prototype.$success = function (message) {
+    Message({
+      message,
+      type: 'success'
+    })
+  }
+
+  Vue.prototype.$error = function (message) {
+    Message({
+      message,
+      type: 'error'
+    })
+  }
+}

+ 0 - 0
static/.gitkeep


+ 27 - 0
test/e2e/custom-assertions/elementCount.js

@@ -0,0 +1,27 @@
+// A custom Nightwatch assertion.
+// The assertion name is the filename.
+// Example usage:
+//
+//   browser.assert.elementCount(selector, count)
+//
+// For more information on custom assertions see:
+// http://nightwatchjs.org/guide#writing-custom-assertions
+
+exports.assertion = function (selector, count) {
+  this.message = 'Testing if element <' + selector + '> has count: ' + count
+  this.expected = count
+  this.pass = function (val) {
+    return val === this.expected
+  }
+  this.value = function (res) {
+    return res.value
+  }
+  this.command = function (cb) {
+    var self = this
+    return this.api.execute(function (selector) {
+      return document.querySelectorAll(selector).length
+    }, [selector], function (res) {
+      cb.call(self, res)
+    })
+  }
+}

+ 46 - 0
test/e2e/nightwatch.conf.js

@@ -0,0 +1,46 @@
+require('babel-register')
+var config = require('../../config')
+
+// http://nightwatchjs.org/gettingstarted#settings-file
+module.exports = {
+  src_folders: ['test/e2e/specs'],
+  output_folder: 'test/e2e/reports',
+  custom_assertions_path: ['test/e2e/custom-assertions'],
+
+  selenium: {
+    start_process: true,
+    server_path: require('selenium-server').path,
+    host: '127.0.0.1',
+    port: 4444,
+    cli_args: {
+      'webdriver.chrome.driver': require('chromedriver').path
+    }
+  },
+
+  test_settings: {
+    default: {
+      selenium_port: 4444,
+      selenium_host: 'localhost',
+      silent: true,
+      globals: {
+        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
+      }
+    },
+
+    chrome: {
+      desiredCapabilities: {
+        browserName: 'chrome',
+        javascriptEnabled: true,
+        acceptSslCerts: true
+      }
+    },
+
+    firefox: {
+      desiredCapabilities: {
+        browserName: 'firefox',
+        javascriptEnabled: true,
+        acceptSslCerts: true
+      }
+    }
+  }
+}

+ 48 - 0
test/e2e/runner.js

@@ -0,0 +1,48 @@
+// 1. start the dev server using production config
+process.env.NODE_ENV = 'testing'
+
+const webpack = require('webpack')
+const DevServer = require('webpack-dev-server')
+
+const webpackConfig = require('../../build/webpack.prod.conf')
+const devConfigPromise = require('../../build/webpack.dev.conf')
+
+let server
+
+devConfigPromise.then(devConfig => {
+  const devServerOptions = devConfig.devServer
+  const compiler = webpack(webpackConfig)
+  server = new DevServer(compiler, devServerOptions)
+  const port = devServerOptions.port
+  const host = devServerOptions.host
+  return server.listen(port, host)
+})
+.then(() => {
+  // 2. run the nightwatch test suite against it
+  // to run in additional browsers:
+  //    1. add an entry in test/e2e/nightwatch.conf.js under "test_settings"
+  //    2. add it to the --env flag below
+  // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
+  // For more information on Nightwatch's config file, see
+  // http://nightwatchjs.org/guide#settings-file
+  let opts = process.argv.slice(2)
+  if (opts.indexOf('--config') === -1) {
+    opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
+  }
+  if (opts.indexOf('--env') === -1) {
+    opts = opts.concat(['--env', 'chrome'])
+  }
+
+  const spawn = require('cross-spawn')
+  const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
+
+  runner.on('exit', function (code) {
+    server.close()
+    process.exit(code)
+  })
+
+  runner.on('error', function (err) {
+    server.close()
+    throw err
+  })
+})

+ 19 - 0
test/e2e/specs/test.js

@@ -0,0 +1,19 @@
+// For authoring Nightwatch tests, see
+// http://nightwatchjs.org/guide#usage
+
+module.exports = {
+  'default e2e tests': function (browser) {
+    // automatically uses dev Server port from /config.index.js
+    // default: http://localhost:8080
+    // see nightwatch.conf.js
+    const devServer = browser.globals.devServerURL
+
+    browser
+      .url(devServer)
+      .waitForElementVisible('#app', 5000)
+      .assert.elementPresent('.hello')
+      .assert.containsText('h1', 'Welcome to Your Vue.js App')
+      .assert.elementCount('img', 1)
+      .end()
+  }
+}

+ 7 - 0
test/unit/.eslintrc

@@ -0,0 +1,7 @@
+{
+  "env": { 
+    "jest": true
+  },
+  "globals": { 
+  }
+}

+ 30 - 0
test/unit/jest.conf.js

@@ -0,0 +1,30 @@
+const path = require('path')
+
+module.exports = {
+  rootDir: path.resolve(__dirname, '../../'),
+  moduleFileExtensions: [
+    'js',
+    'json',
+    'vue'
+  ],
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  transform: {
+    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
+    '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
+  },
+  testPathIgnorePatterns: [
+    '<rootDir>/test/e2e'
+  ],
+  snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
+  setupFiles: ['<rootDir>/test/unit/setup'],
+  mapCoverage: true,
+  coverageDirectory: '<rootDir>/test/unit/coverage',
+  collectCoverageFrom: [
+    'src/**/*.{js,vue}',
+    '!src/main.js',
+    '!src/router/index.js',
+    '!**/node_modules/**'
+  ]
+}

+ 3 - 0
test/unit/setup.js

@@ -0,0 +1,3 @@
+import Vue from 'vue'
+
+Vue.config.productionTip = false

+ 11 - 0
test/unit/specs/HelloWorld.spec.js

@@ -0,0 +1,11 @@
+import Vue from 'vue'
+import HelloWorld from '@/components/HelloWorld'
+
+describe('HelloWorld.vue', () => {
+  it('should render correct contents', () => {
+    const Constructor = Vue.extend(HelloWorld)
+    const vm = new Constructor().$mount()
+    expect(vm.$el.querySelector('.hello h1').textContent)
+      .toEqual('Welcome to Your Vue.js App')
+  })
+})