rabin %!s(int64=7) %!d(string=hai) anos
achega
ca6164fd5c
Modificáronse 100 ficheiros con 8585 adicións e 0 borrados
  1. 21 0
      LICENSE
  2. 177 0
      README-en.md
  3. 1 0
      README.md
  4. 41 0
      build/build.js
  5. 45 0
      build/check-versions.js
  6. 9 0
      build/dev-client.js
  7. 84 0
      build/dev-server.js
  8. 71 0
      build/utils.js
  9. 12 0
      build/vue-loader.conf.js
  10. 86 0
      build/webpack.base.conf.js
  11. 50 0
      build/webpack.dev.conf.js
  12. 121 0
      build/webpack.prod.conf.js
  13. 5 0
      config/dev.env.js
  14. 39 0
      config/index.js
  15. 5 0
      config/prod.env.js
  16. 5 0
      config/sit.env.js
  17. BIN=BIN
      favicon.ico
  18. 17 0
      index.html
  19. 97 0
      package.json
  20. 15 0
      src/App.vue
  21. 16 0
      src/api/article.js
  22. 17 0
      src/api/article_table.js
  23. 29 0
      src/api/login.js
  24. 8 0
      src/api/qiniu.js
  25. 9 0
      src/api/remoteSearch.js
  26. BIN=BIN
      src/assets/401_images/401.gif
  27. BIN=BIN
      src/assets/404_images/404.png
  28. BIN=BIN
      src/assets/404_images/404_cloud.png
  29. BIN=BIN
      src/assets/custom-theme/fonts/element-icons.ttf
  30. BIN=BIN
      src/assets/custom-theme/fonts/element-icons.woff
  31. 0 0
      src/assets/custom-theme/index.css
  32. 199 0
      src/assets/echarts-macarons.js
  33. 0 0
      src/assets/iconfont/iconfont.js
  34. 110 0
      src/components/BackToTop/index.vue
  35. 113 0
      src/components/Charts/keyboard.vue
  36. 150 0
      src/components/Charts/keyboard2.vue
  37. 222 0
      src/components/Charts/lineMarker.vue
  38. 268 0
      src/components/Charts/mixChart.vue
  39. 296 0
      src/components/Dropzone/index.vue
  40. 56 0
      src/components/ErrLog/index.vue
  41. 46 0
      src/components/Hamburger/index.vue
  42. 22 0
      src/components/Icon-svg/index.vue
  43. 697 0
      src/components/ImageCropper/index.vue
  44. 41 0
      src/components/ImageCropper/lang.js
  45. 691 0
      src/components/ImageCropper/upload.css
  46. 58 0
      src/components/ImageCropper/utils.js
  47. 297 0
      src/components/MDinput/index.vue
  48. 114 0
      src/components/MdEditor/index.vue
  49. 140 0
      src/components/PanThumb/index.vue
  50. 53 0
      src/components/Screenfull/index.vue
  51. 44 0
      src/components/SplitPane/Pane.vue
  52. 72 0
      src/components/SplitPane/Resizer.vue
  53. 111 0
      src/components/SplitPane/index.vue
  54. 74 0
      src/components/Sticky/index.vue
  55. 174 0
      src/components/Tinymce/index.vue
  56. 70 0
      src/components/TodoList/Todo.vue
  57. 318 0
      src/components/TodoList/index.scss
  58. 116 0
      src/components/TodoList/index.vue
  59. 122 0
      src/components/Upload/singleImage.vue
  60. 119 0
      src/components/Upload/singleImage2.vue
  61. 146 0
      src/components/Upload/singleImage3.vue
  62. 64 0
      src/components/jsonEditor/index.vue
  63. 158 0
      src/components/twoDndList/index.vue
  64. 91 0
      src/directive/sticky.js
  65. 26 0
      src/directive/waves.css
  66. 47 0
      src/directive/waves.js
  67. 103 0
      src/filters/index.js
  68. 109 0
      src/main.js
  69. 37 0
      src/mock/article.js
  70. 44 0
      src/mock/article_table.js
  71. 25 0
      src/mock/index.js
  72. 41 0
      src/mock/login.js
  73. 24 0
      src/mock/remoteSearch.js
  74. 1 0
      src/router/_import_development.js
  75. 1 0
      src/router/_import_production.js
  76. 161 0
      src/router/index.js
  77. 13 0
      src/store/errLog.js
  78. 14 0
      src/store/getters.js
  79. 19 0
      src/store/index.js
  80. 49 0
      src/store/modules/app.js
  81. 62 0
      src/store/modules/permission.js
  82. 133 0
      src/store/modules/user.js
  83. 103 0
      src/styles/btn.scss
  84. 82 0
      src/styles/element-ui.scss
  85. 271 0
      src/styles/index.scss
  86. 60 0
      src/styles/mixin.scss
  87. 77 0
      src/styles/sidebar.scss
  88. 15 0
      src/utils/auth.js
  89. 8 0
      src/utils/createUniqueString.js
  90. 66 0
      src/utils/fetch.js
  91. 270 0
      src/utils/index.js
  92. 27 0
      src/utils/openWindow.js
  93. 41 0
      src/utils/validate.js
  94. 179 0
      src/vendor/Blob.js
  95. 141 0
      src/vendor/Export2Excel.js
  96. 7 0
      src/views/charts/index.vue
  97. 24 0
      src/views/charts/keyboard.vue
  98. 24 0
      src/views/charts/keyboard2.vue
  99. 24 0
      src/views/charts/line.vue
  100. 25 0
      src/views/charts/mixChart.vue

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 PanJiaChen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 177 - 0
README-en.md

@@ -0,0 +1,177 @@
+## Intro
+
+> In the past half year, I have been building a backend for management dashboard using Vue. Though the backend has contained greater than 70 pages and over 10 permissions, it still takes insignificant effort to maintain the project. So I decide to make it open source so as to share my development experience and progress on backend. The tech stack is mainly [Vue.js](https://github.com/vuejs/vue)+[Element](https://github.com/ElemeFE/element)+[axios](https://github.com/mzabriskie/axios). Since it's a personal project, all data requests are simulated with [Mock.js](https://github.com/nuysoft/Mock). **Note:** if anyone wants to modify or develop based on this project, please remove the mock files.
+
+**Live demo:** http://panjiachen.github.io/vue-element-admin
+
+**Note: element-ui@1.3.3 is used in the project, so vue 2.3.0+ is required.**
+
+More tutorials incoming. Including articles on:
+
+- How to build structure of a backend dashboard project from scratch
+- How to make a complete user system (e.g. permission authentication, two-factor authentication)
+- How to package components (e.g. rich text)
+- How to integrate with [Qiniu](https://www.qiniu.com/)
+- Other development experience on backend
+
+Join the group on QQ 591724180.
+
+**Tutorials:**
+
+- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki)
+- [Step by step instructions on playing with backend using Vue Part 1 - Fundamentals](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
+- [Step by step instructions on playing with backend using Vue Part 2 - Login permission](https://juejin.im/post/591aa14f570c35006961acac)
+- [Step by step instructions on packaging a Vue component](https://segmentfault.com/a/1190000009090836)
+
+**Please read the Wiki and articles above before creating any issue. Feel free to contribute by making a pull request.**
+
+## Features
+
+- Login/Logout
+- Permission authentication
+- Sidebar
+- Breadcrumb
+- Rich text editor
+- Markdown editor
+- JSON editor
+- Drag & drop list
+- SplitPane
+- Dropzone
+- Sticky
+- CountTo
+- ECharts
+- 401, 404 error page
+- Error log
+- Exporting to Excel
+- Table example
+- Interactive table example
+- Drag & drop table example
+- Form example
+- Multi-environments distribution
+- Dashboard
+- Two-factor authentication
+- Collapsing sidebar (support nested routes)
+- Mock data
+- cache tabs example
+- screenfull
+- markdown2html
+- views-tab
+
+## Development
+
+```bash
+# Clone project
+git clone https://github.com/PanJiaChen/vue-element-admin.git
+
+# Install dependencies
+npm install
+
+# Or (not recommended for cnpm due to unknown bugs, use taobao mirror instead)
+npm install --registry=https://registry.npm.taobao.org
+
+# Run local dev server
+npm run dev
+```
+
+Visit in browser: http://localhost:9527
+
+## Distribution
+
+```bash
+# Build staged environment with webpack-bundle-analyzer
+npm run build:sit-preview
+
+# Build production environment
+npm run build:prod
+```
+
+## Directory structure
+
+```
+├── build                      // build 
+├── config                     // config
+├── src                        // source code
+│   ├── api                    // all requests
+│   ├── assets                 // static resource like themes, fonts
+│   ├── components             // global public components
+│   ├── directive              // global directive
+│   ├── filters                // global filters
+│   ├── mock                   // mock data
+│   ├── router                 // router
+│   ├── store                  // global status management
+│   ├── styles                 // global styles
+│   ├── utils                  // global public functions
+│   ├── view                   // view
+│   ├── App.vue                // entry view
+│   └── main.js                // entry for loading components, initialization
+├── static                     // third-party libraries not packed with Webpack
+│   ├── jquery
+│   └── Tinymce                // rich text
+├── .babelrc                   // babel-loader config
+├── eslintrc.js                // eslint config
+├── .gitignore                 // gitignore
+├── favicon.ico                // favicon
+├── index.html                 // html template
+└── package.json               // package.json
+```
+
+## Changelog
+Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases).
+
+## State Management
+
+Only status of user and app configuration is managed by Vuex. Other data are managed by their own business pages.
+
+## Demo
+
+#### Two-factor authentication, supporting WeChat and QQ
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/2login.gif)
+
+#### Realtime switching themes
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/theme.gif)
+
+#### tabs
+
+![tabs](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/tabs.gif)<br />
+
+#### Collapsing sidebar
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/leftmenu.gif)
+
+#### Drag & drop table
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/order.gif)
+
+#### Interactive table
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/dynamictable.gif)
+
+#### Uploading cropped avatar
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/uploadAvatar.gif)
+
+#### Error log
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/errorlog.gif)
+
+#### Rich text (integrated with Qiniu, watermark and customization)
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/editor.gif)
+
+#### Packaging table component
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/table.gif)
+
+#### Charts
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/echarts.gif)
+
+#### Exporting to Excel
+
+![](https://github.com/PanJiaChen/vue-element-admin/blob/master/gifs/excel.png)
+
+#### More
+
+http://panjiachen.github.io/vue-element-admin

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# 后台模板

+ 41 - 0
build/build.js

@@ -0,0 +1,41 @@
+require('./check-versions')();
+var server = require('pushstate-server');
+var opn = require('opn')
+var ora = require('ora')
+var rm = require('rimraf')
+var path = require('path')
+var chalk = require('chalk')
+var webpack = require('webpack');
+var config = require('../config');
+var webpackConfig = require('./webpack.prod.conf');
+
+console.log(process.env.NODE_ENV)
+
+var spinner = ora('building for ' + process.env.NODE_ENV + '...')
+spinner.start()
+
+
+rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
+    if (err) throw err
+    webpack(webpackConfig, function (err, stats) {
+        spinner.stop()
+        if (err) throw err
+        process.stdout.write(stats.toString({
+                colors: true,
+                modules: false,
+                children: false,
+                chunks: false,
+                chunkModules: false
+            }) + '\n\n')
+
+        console.log(chalk.cyan('  Build complete.\n'))
+        if(process.env.npm_config_preview){
+            server.start({
+                port: 9528,
+                directory: './dist',
+                file: '/index.html'
+            });
+            console.log('> Listening at ' +  'http://localhost:9528' + '\n')
+        }
+    })
+})

+ 45 - 0
build/check-versions.js

@@ -0,0 +1,45 @@
+var chalk = require('chalk')
+var semver = require('semver')
+var packageConfig = require('../package.json')
+
+function exec(cmd) {
+    return require('child_process').execSync(cmd).toString().trim()
+}
+
+var versionRequirements = [
+    {
+        name: 'node',
+        currentVersion: semver.clean(process.version),
+        versionRequirement: packageConfig.engines.node
+    },
+    {
+        name: 'npm',
+        currentVersion: exec('npm --version'),
+        versionRequirement: packageConfig.engines.npm
+    }
+]
+
+module.exports = function () {
+    var warnings = []
+    for (var i = 0; i < versionRequirements.length; i++) {
+        var mod = versionRequirements[i]
+        if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
+            warnings.push(mod.name + ': ' +
+                chalk.red(mod.currentVersion) + ' should be ' +
+                chalk.green(mod.versionRequirement)
+            )
+        }
+    }
+
+    if (warnings.length) {
+        console.log('')
+        console.log(chalk.yellow('To use this template, you must update following to modules:'))
+        console.log()
+        for (var i = 0; i < warnings.length; i++) {
+            var warning = warnings[i]
+            console.log('  ' + warning)
+        }
+        console.log()
+        process.exit(1)
+    }
+}

+ 9 - 0
build/dev-client.js

@@ -0,0 +1,9 @@
+/* eslint-disable */
+require('eventsource-polyfill')
+var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
+
+hotClient.subscribe(function (event) {
+  if (event.action === 'reload') {
+    window.location.reload()
+  }
+})

+ 84 - 0
build/dev-server.js

@@ -0,0 +1,84 @@
+require('./check-versions')(); // 检查 Node 和 npm 版本
+var config = require('../config');
+if (!process.env.NODE_ENV) {
+    process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
+}
+
+var opn = require('opn')
+var path = require('path');
+var express = require('express');
+var webpack = require('webpack');
+var proxyMiddleware = require('http-proxy-middleware');
+var webpackConfig = require('./webpack.dev.conf');
+
+// default port where dev server listens for incoming traffic
+var port = process.env.PORT || config.dev.port;
+// automatically open browser, if not set will be false
+var autoOpenBrowser = !!config.dev.autoOpenBrowser;
+// Define HTTP proxies to your custom API backend
+// https://github.com/chimurai/http-proxy-middleware
+var proxyTable = config.dev.proxyTable;
+
+var app = express();
+var compiler = webpack(webpackConfig);
+
+var devMiddleware = require('webpack-dev-middleware')(compiler, {
+    publicPath: webpackConfig.output.publicPath,
+    quiet: true
+});
+
+var hotMiddleware = require('webpack-hot-middleware')(compiler, {
+    log: () => {
+    }
+});
+
+// force page reload when html-webpack-plugin template changes
+compiler.plugin('compilation', function (compilation) {
+    compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
+        hotMiddleware.publish({action: 'reload'});
+        cb()
+    })
+});
+
+// compiler.apply(new DashboardPlugin());
+
+// proxy api requests
+Object.keys(proxyTable).forEach(function (context) {
+    var options = proxyTable[context]
+    if (typeof options === 'string') {
+        options = {target: options}
+    }
+    app.use(proxyMiddleware(options.filter || context, options))
+});
+
+// handle fallback for HTML5 history API
+app.use(require('connect-history-api-fallback')());
+
+// serve webpack bundle output
+app.use(devMiddleware);
+
+// enable hot-reload and state-preserving
+// compilation error display
+app.use(hotMiddleware);
+
+// serve pure static assets
+var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory);
+app.use(staticPath, express.static('./static'));
+
+var uri = 'http://localhost:' + port
+
+devMiddleware.waitUntilValid(function () {
+    console.log('> Listening at ' + uri + '\n')
+});
+
+module.exports = app.listen(port, function (err) {
+    if (err) {
+        console.log(err);
+        return
+    }
+
+    // when env is testing, don't need open it
+    if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
+        opn(uri)
+    }
+});

+ 71 - 0
build/utils.js

@@ -0,0 +1,71 @@
+var path = require('path')
+var config = require('../config')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+
+exports.assetsPath = function (_path) {
+  var assetsSubDirectory = process.env.NODE_ENV === 'production'
+    ? config.build.assetsSubDirectory
+    : config.dev.assetsSubDirectory
+  return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function (options) {
+  options = options || {}
+
+  var cssLoader = {
+    loader: 'css-loader',
+    options: {
+      minimize: process.env.NODE_ENV === 'production',
+      sourceMap: options.sourceMap
+    }
+  }
+
+  // generate loader string to be used with extract text plugin
+  function generateLoaders (loader, loaderOptions) {
+    var loaders = [cssLoader]
+    if (loader) {
+      loaders.push({
+        loader: loader + '-loader',
+        options: Object.assign({}, loaderOptions, {
+          sourceMap: options.sourceMap
+        })
+      })
+    }
+
+    // Extract CSS when that option is specified
+    // (which is the case during production build)
+    if (options.extract) {
+      return ExtractTextPlugin.extract({
+        use: loaders,
+        fallback: 'vue-style-loader'
+      })
+    } else {
+      return ['vue-style-loader'].concat(loaders)
+    }
+  }
+
+  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
+  return {
+    css: generateLoaders(),
+    postcss: generateLoaders(),
+    less: generateLoaders('less'),
+    sass: generateLoaders('sass', { indentedSyntax: true }),
+    scss: generateLoaders('sass'),
+    stylus: generateLoaders('stylus'),
+    styl: generateLoaders('stylus')
+  }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function (options) {
+  var output = []
+  var loaders = exports.cssLoaders(options)
+  for (var extension in loaders) {
+    var loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      use: loader
+    })
+  }
+  return output
+}

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

@@ -0,0 +1,12 @@
+var utils = require('./utils')
+var config = require('../config')
+var isProduction = process.env.NODE_ENV === 'production'
+
+module.exports = {
+    loaders: utils.cssLoaders({
+        sourceMap: isProduction
+            ? config.build.productionSourceMap
+            : config.dev.cssSourceMap,
+        extract: isProduction
+    })
+}

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

@@ -0,0 +1,86 @@
+var path = require('path')
+var utils = require('./utils')
+var config = require('../config')
+var vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+module.exports = {
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    filename: '[name].js',
+    publicPath: process.env.NODE_ENV !== 'development' ? config.build.assetsPublicPath : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      'vue$': 'vue/dist/vue.esm.js',
+      '@': resolve('src'),
+      'src': path.resolve(__dirname, '../src'),
+      'assets': path.resolve(__dirname, '../src/assets'),
+      'components': path.resolve(__dirname, '../src/components'),
+      'views': path.resolve(__dirname, '../src/views'),
+      'styles': path.resolve(__dirname, '../src/styles'),
+      'api': path.resolve(__dirname, '../src/api'),
+      'utils': path.resolve(__dirname, '../src/utils'),
+      'store': path.resolve(__dirname, '../src/store'),
+      'router': path.resolve(__dirname, '../src/router'),
+      'mock': path.resolve(__dirname, '../src/mock'),
+      'vendor': path.resolve(__dirname, '../src/vendor'),
+      'static': path.resolve(__dirname, '../static')
+    }
+  },
+  externals: {
+    jquery: 'jQuery'
+  },
+  module: {
+    rules: [
+      // {
+      //     test: /\.(js|vue)$/,
+      //     loader: 'eslint-loader',
+      //     enforce: "pre",
+      //     include: [resolve('src'), resolve('test')],
+      //     options: {
+      //         formatter: require('eslint-friendly-formatter')
+      //     }
+      // },
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader?cacheDirectory',
+        include: [resolve('src'), resolve('test')]
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        query: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        query: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  //注入全局mixin
+  // sassResources: path.join(__dirname, '../src/styles/mixin.scss'),
+  // sassLoader: {
+  //     data:  path.join(__dirname, '../src/styles/index.scss')
+  // },
+}
+

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

@@ -0,0 +1,50 @@
+var utils = require('./utils')
+var path = require('path')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+
+// add hot-reload related code to entry chunks
+Object.keys(baseWebpackConfig.entry).forEach(function (name) {
+  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
+})
+
+function resolveApp(relativePath) {
+  return path.resolve(relativePath);
+}
+
+module.exports = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.dev.cssSourceMap
+    })
+  },
+  // cheap-source-map is faster for development
+  devtool: '#cheap-source-map',
+  cache: true,
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': config.dev.env
+    }),
+    new webpack.ProvidePlugin({
+      $: 'jquery',
+      'jQuery': 'jquery'
+    }),
+    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
+    new webpack.HotModuleReplacementPlugin(),
+    new webpack.NoEmitOnErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.html',
+      favicon: resolveApp('favicon.ico'),
+      inject: true,
+      path: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
+    }),
+    new FriendlyErrorsPlugin()
+  ]
+})
+

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

@@ -0,0 +1,121 @@
+var path = require('path')
+var utils = require('./utils')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+
+var env = process.env.NODE_ENV === 'production' ? config.build.prodEnv : config.build.sitEnv
+
+function resolveApp(relativePath) {
+  return path.resolve(relativePath);
+}
+
+var webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? '#source-map' : false,
+  output: {
+    path: config.build.assetsRoot,
+    filename: utils.assetsPath('js/[name].[chunkhash].js'),
+    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
+    publicPath: config.build.assetsPublicPath
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress: {
+        warnings: false
+      },
+      sourceMap: true
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash].css')
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin(),
+    // 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: 'index.html',
+      template: 'index.html',
+      inject: true,
+      favicon: resolveApp('favicon.ico'),
+      minify: {
+        removeComments: true,
+        collapseWhitespace: true,
+        removeRedundantAttributes: true,
+        useShortDoctype: true,
+        removeEmptyAttributes: true,
+        removeStyleLinkTypeAttributes: true,
+        keepClosingSlash: true,
+        minifyJS: true,
+        minifyCSS: true,
+        minifyURLs: true
+      },
+      path: config.build.assetsPublicPath + config.build.assetsSubDirectory,
+      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+      chunksSortMode: 'dependency'
+    }),
+    // cache Module Identifiers
+    new webpack.HashedModuleIdsPlugin(),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks: function (module, count) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf(
+            path.join(__dirname, '../node_modules')
+          ) === 0
+        )
+      }
+    }),
+    // split echarts into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      async: 'echarts',
+      minChunks(module) {
+        var context = module.context;
+        return context && (context.indexOf('echarts') >= 0 || context.indexOf('zrender') >= 0);
+      }
+    }),
+    // extract webpack runtime and module manifest to its own file in order to
+    // prevent vendor hash from being updated whenever app bundle is updated
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'manifest',
+      chunks: ['vendor']
+    }),
+    // copy custom static assets
+    new CopyWebpackPlugin([{
+      from: path.resolve(__dirname, '../static'),
+      to: config.build.assetsSubDirectory,
+      ignore: ['.*']
+    }]),
+    new webpack.ProvidePlugin({
+      $: 'jquery',
+      'jQuery': 'jquery'
+    })
+  ]
+})
+if (config.build.bundleAnalyzerReport) {
+  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+module.exports = webpackConfig
+

+ 5 - 0
config/dev.env.js

@@ -0,0 +1,5 @@
+module.exports = {
+    NODE_ENV: '"development"',
+    BASE_API: '"https://api-dev"',
+    APP_ORIGIN: '"https://wallstreetcn.com"'
+}

+ 39 - 0
config/index.js

@@ -0,0 +1,39 @@
+// see http://vuejs-templates.github.io/webpack for documentation.
+var path = require('path')
+
+module.exports = {
+    build: {
+        sitEnv: require('./sit.env'),
+        prodEnv: require('./prod.env'),
+        index: path.resolve(__dirname, '../dist/index.html'),
+        assetsRoot: path.resolve(__dirname, '../dist'),
+        assetsSubDirectory: 'static',
+        assetsPublicPath: './',          //请根据自己路径配置更改
+        productionSourceMap: false,
+        // Gzip off by default as many popular static hosts such as
+        // Surge or Netlify already gzip all static assets for you.
+        // Before setting to `true`, make sure to:
+        // npm install --save-dev compression-webpack-plugin
+        productionGzip: false,
+        productionGzipExtensions: ['js', 'css'],
+        // Run the build command with an extra argument to
+        // View the bundle analyzer report after build finishes:
+        // `npm run build --report`
+        // Set to `true` or `false` to always turn it on or off
+        bundleAnalyzerReport: process.env.npm_config_report
+    },
+    dev: {
+        env: require('./dev.env'),
+        port: 9527,
+        autoOpenBrowser: true,
+        assetsSubDirectory: 'static',
+        assetsPublicPath: '/',
+        proxyTable: {},
+        // CSS Sourcemaps off by default because relative paths are "buggy"
+        // with this option, according to the CSS-Loader README
+        // (https://github.com/webpack/css-loader#sourcemaps)
+        // In our experience, they generally work as expected,
+        // just be aware of this issue when enabling this option.
+        cssSourceMap: false
+    }
+}

+ 5 - 0
config/prod.env.js

@@ -0,0 +1,5 @@
+module.exports = {
+    NODE_ENV: '"production"',
+    BASE_API: '"https://api-prod"',
+    APP_ORIGIN: '"https://wallstreetcn.com"'
+};

+ 5 - 0
config/sit.env.js

@@ -0,0 +1,5 @@
+module.exports = {
+    NODE_ENV: '"production"',
+    BASE_API: '"https://api-sit"',
+    APP_ORIGIN: '"https://wallstreetcn.com"'
+};

BIN=BIN
favicon.ico


+ 17 - 0
index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="renderer" content="webkit">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <title>Juicy</title>
+</head>
+<body>
+<script src=<%= htmlWebpackPlugin.options.path %>/jquery.min.js></script>
+<script src=<%= htmlWebpackPlugin.options.path %>/tinymce/tinymce.min.js></script>
+<div id="app"></div>
+<!-- built files will be auto injected -->
+</body>
+
+</html>

+ 97 - 0
package.json

@@ -0,0 +1,97 @@
+{
+  "name": "dever-admin",
+  "version": "1.0.0",
+  "description": "A Vue.js admin",
+  "author": "rabin",
+  "license": "MIT",
+  "private": true,
+  "scripts": {
+    "dev": "node build/dev-server.js",
+    "build:prod": "cross-env NODE_ENV=production node build/build.js",
+    "build:sit": "cross-env NODE_ENV=sit node build/build.js",
+    "build:sit-preview": "cross-env NODE_ENV=sit npm_config_preview=true  npm_config_report=true node build/build.js",
+    "lint": "eslint --ext .js,.vue src"
+  },
+  "dependencies": {
+    "axios": "0.16.2",
+    "codemirror": "5.26.0",
+    "dropzone": "5.1.0",
+    "echarts": "3.6.2",
+    "element-ui": "1.4.1",
+    "file-saver": "1.3.3",
+    "jquery": "3.1.1",
+    "js-cookie": "2.1.4",
+    "jsonlint": "1.6.2",
+    "mockjs": "1.0.1-beta3",
+    "normalize.css": "3.0.2",
+    "nprogress": "0.2.0",
+    "screenfull": "3.2.2",
+    "showdown": "1.7.1",
+    "simplemde": "1.11.2",
+    "sortablejs": "1.5.1",
+    "vue": "2.4.2",
+    "vue-count-to": "1.0.5",
+    "vue-multiselect": "2.0.0-beta.15",
+    "vue-router": "2.5.3",
+    "vuedraggable": "2.13.1",
+    "vuex": "2.3.1",
+    "xlsx": "^0.10.8"
+  },
+  "devDependencies": {
+    "autoprefixer": "7.1.1",
+    "babel-core": "6.25.0",
+    "babel-eslint": "7.2.3",
+    "babel-loader": "7.0.0",
+    "babel-plugin-transform-runtime": "6.23.0",
+    "babel-preset-env": "1.5.2",
+    "babel-preset-stage-2": "6.24.1",
+    "babel-register": "6.24.1",
+    "chalk": "1.1.3",
+    "connect-history-api-fallback": "1.3.0",
+    "copy-webpack-plugin": "4.0.1",
+    "cross-env": "5.0.1",
+    "css-loader": "0.28.4",
+    "eslint": "3.19.0",
+    "eslint-friendly-formatter": "3.0.0",
+    "eslint-import-resolver-webpack": "0.8.1",
+    "eslint-loader": "1.7.1",
+    "eslint-plugin-html": "3.0.0",
+    "eslint-plugin-import": "2.3.0",
+    "eventsource-polyfill": "0.9.6",
+    "express": "4.15.3",
+    "extract-text-webpack-plugin": "2.1.2",
+    "file-loader": "0.11.2",
+    "friendly-errors-webpack-plugin": "1.6.1",
+    "function-bind": "1.1.0",
+    "html-webpack-plugin": "2.28.0",
+    "http-proxy-middleware": "0.17.4",
+    "node-sass": "^4.5.0",
+    "opn": "4.0.2",
+    "optimize-css-assets-webpack-plugin": "1.3.0",
+    "ora": "1.1.0",
+    "pushstate-server": "2.1.0",
+    "rimraf": "2.6.0",
+    "sass-loader": "6.0.5",
+    "script-loader": "0.7.0",
+    "semver": "5.3.0",
+    "style-loader": "0.17.0",
+    "url-loader": "0.5.8",
+    "vue-loader": "12.2.1",
+    "vue-style-loader": "3.0.1",
+    "vue-template-compiler": "2.4.2",
+    "webpack": "2.6.1",
+    "webpack-bundle-analyzer": "2.8.2",
+    "webpack-dev-middleware": "1.10.2",
+    "webpack-hot-middleware": "2.18.0",
+    "webpack-merge": "4.1.0"
+  },
+  "engines": {
+    "node": ">= 4.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserlist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 15 - 0
src/App.vue

@@ -0,0 +1,15 @@
+<template>
+	<div id="app">
+		<router-view></router-view>
+	</div>
+</template>
+
+<script>
+  export default{
+    name: 'APP'
+  }
+</script>
+
+<style lang="scss">
+  @import './styles/index.scss'; // 全局自定义的css样式
+</style>

+ 16 - 0
src/api/article.js

@@ -0,0 +1,16 @@
+import fetch from 'utils/fetch';
+
+export function getList() {
+  return fetch({
+    url: '/article/list',
+    method: 'get'
+  });
+}
+
+export function getArticle() {
+  return fetch({
+    url: '/article/detail',
+    method: 'get'
+  });
+}
+

+ 17 - 0
src/api/article_table.js

@@ -0,0 +1,17 @@
+import fetch from 'utils/fetch';
+
+export function fetchList(query) {
+  return fetch({
+    url: '/article_table/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function fetchPv(pv) {
+  return fetch({
+    url: '/article_table/pv',
+    method: 'get',
+    params: { pv }
+  });
+}

+ 29 - 0
src/api/login.js

@@ -0,0 +1,29 @@
+import fetch from 'utils/fetch';
+
+export function loginByEmail(email, password) {
+  const data = {
+    email,
+    password
+  };
+  return fetch({
+    url: '/login/loginbyemail',
+    method: 'post',
+    data
+  });
+}
+
+export function logout() {
+  return fetch({
+    url: '/login/logout',
+    method: 'post'
+  });
+}
+
+export function getInfo(token) {
+  return fetch({
+    url: '/user/info',
+    method: 'get',
+    params: { token }
+  });
+}
+

+ 8 - 0
src/api/qiniu.js

@@ -0,0 +1,8 @@
+import fetch from 'utils/fetch';
+
+export function getToken() {
+  return fetch({
+    url: '/qiniu/upload/token', // 假地址 自行替换
+    method: 'get'
+  });
+}

+ 9 - 0
src/api/remoteSearch.js

@@ -0,0 +1,9 @@
+import fetch from 'utils/fetch';
+
+export function userSearch(name) {
+  return fetch({
+    url: '/search/user',
+    method: 'get',
+    params: { name }
+  });
+}

BIN=BIN
src/assets/401_images/401.gif


BIN=BIN
src/assets/404_images/404.png


BIN=BIN
src/assets/404_images/404_cloud.png


BIN=BIN
src/assets/custom-theme/fonts/element-icons.ttf


BIN=BIN
src/assets/custom-theme/fonts/element-icons.woff


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/assets/custom-theme/index.css


+ 199 - 0
src/assets/echarts-macarons.js

@@ -0,0 +1,199 @@
+/* eslint-disable */
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(['exports', 'echarts'], factory);
+    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
+        // CommonJS
+        factory(exports, require('echarts'));
+    } else {
+        // Browser globals
+        factory({}, root.echarts);
+    }
+}(this, function (exports, echarts) {
+    var log = function (msg) {
+        if (typeof console !== 'undefined') {
+            console && console.error && console.error(msg);
+        }
+    };
+    if (!echarts) {
+        log('ECharts is not Loaded');
+        return;
+    }
+
+    var colorPalette = [
+        '#2ec7c9','#b6a2de','#5ab1ef','#ffb980','#d87a80',
+        '#8d98b3','#e5cf0d','#97b552','#95706d','#dc69aa',
+        '#07a2a4','#9a7fd1','#588dd5','#f5994e','#c05050',
+        '#59678c','#c9ab00','#7eb00a','#6f5553','#c14089'
+    ];
+
+
+    var theme = {
+        color: colorPalette,
+
+        title: {
+            textStyle: {
+                fontWeight: 'normal',
+                color: '#008acd'
+            }
+        },
+
+        visualMap: {
+            itemWidth: 15,
+            color: ['#5ab1ef','#e0ffff']
+        },
+
+        toolbox: {
+            iconStyle: {
+                normal: {
+                    borderColor: colorPalette[0]
+                }
+            }
+        },
+
+        tooltip: {
+            backgroundColor: 'rgba(50,50,50,0.5)',
+            axisPointer : {
+                type : 'line',
+                lineStyle : {
+                    color: '#008acd'
+                },
+                crossStyle: {
+                    color: '#008acd'
+                },
+                shadowStyle : {
+                    color: 'rgba(200,200,200,0.2)'
+                }
+            }
+        },
+
+        dataZoom: {
+            dataBackgroundColor: '#efefff',
+            fillerColor: 'rgba(182,162,222,0.2)',
+            handleColor: '#008acd'
+        },
+
+        grid: {
+            borderColor: '#eee'
+        },
+
+        categoryAxis: {
+            axisLine: {
+                lineStyle: {
+                    color: '#008acd'
+                }
+            },
+            splitLine: {
+                lineStyle: {
+                    color: ['#eee']
+                }
+            }
+        },
+
+        valueAxis: {
+            axisLine: {
+                lineStyle: {
+                    color: '#008acd'
+                }
+            },
+            splitArea : {
+                show : true,
+                areaStyle : {
+                    color: ['rgba(250,250,250,0.1)','rgba(200,200,200,0.1)']
+                }
+            },
+            splitLine: {
+                lineStyle: {
+                    color: ['#eee']
+                }
+            }
+        },
+
+        timeline : {
+            lineStyle : {
+                color : '#008acd'
+            },
+            controlStyle : {
+                normal : { color : '#008acd'},
+                emphasis : { color : '#008acd'}
+            },
+            symbol : 'emptyCircle',
+            symbolSize : 3
+        },
+
+        line: {
+            smooth : true,
+            symbol: 'emptyCircle',
+            symbolSize: 3
+        },
+
+        candlestick: {
+            itemStyle: {
+                normal: {
+                    color: '#d87a80',
+                    color0: '#2ec7c9',
+                    lineStyle: {
+                        color: '#d87a80',
+                        color0: '#2ec7c9'
+                    }
+                }
+            }
+        },
+
+        scatter: {
+            symbol: 'circle',
+            symbolSize: 4
+        },
+
+        map: {
+            label: {
+                normal: {
+                    textStyle: {
+                        color: '#d87a80'
+                    }
+                }
+            },
+            itemStyle: {
+                normal: {
+                    borderColor: '#eee',
+                    areaColor: '#ddd'
+                },
+                emphasis: {
+                    areaColor: '#fe994e'
+                }
+            }
+        },
+
+        graph: {
+            color: colorPalette
+        },
+
+        gauge : {
+            axisLine: {
+                lineStyle: {
+                    color: [[0.2, '#2ec7c9'],[0.8, '#5ab1ef'],[1, '#d87a80']],
+                    width: 10
+                }
+            },
+            axisTick: {
+                splitNumber: 10,
+                length :15,
+                lineStyle: {
+                    color: 'auto'
+                }
+            },
+            splitLine: {
+                length :22,
+                lineStyle: {
+                    color: 'auto'
+                }
+            },
+            pointer : {
+                width : 5
+            }
+        }
+    };
+
+    echarts.registerTheme('macarons', theme);
+}));

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/assets/iconfont/iconfont.js


+ 110 - 0
src/components/BackToTop/index.vue

@@ -0,0 +1,110 @@
+<template>
+  <transition :name="transitionName">
+    <div class="back-to-top" @click="backToTop" v-show="visible" :style="customStyle">
+      <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height: 16px; width: 16px;">
+        <title>回到顶部</title>
+        <g>
+          <path d="M12.036 15.59c0 .55-.453.995-.997.995H5.032c-.55 0-.997-.445-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29c.39-.39 1.026-.385 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" fill-rule="evenodd"></path>
+        </g>
+      </svg>
+    </div>
+  </transition>
+</template>
+
+<script>
+  export default {
+    name: 'BackToTop',
+    props: {
+      visibilityHeight: {
+        type: Number,
+        default: 400
+      },
+      backPosition: {
+        type: Number,
+        default: 0
+      },
+      customStyle: {
+        type: Object,
+        default: {
+          right: '50px',
+          bottom: '50px',
+          width: '40px',
+          height: '40px',
+          'border-radius': '4px',
+          'line-height': '45px',
+          background: '#e7eaf1'
+        }
+      },
+      transitionName: {
+        type: String,
+        default: 'fade'
+      }
+    },
+    data() {
+      return {
+        visible: false,
+        interval: null
+      }
+    },
+    mounted() {
+      window.addEventListener('scroll', this.handleScroll);
+    },
+    beforeDestroy() {
+      window.removeEventListener('scroll', this.handleScroll);
+      if (this.interval) {
+        clearInterval(this.interval);
+      }
+    },
+    methods: {
+      handleScroll() {
+        this.visible = window.pageYOffset > this.visibilityHeight;
+      },
+      backToTop() {
+        const start = window.pageYOffset;
+        let i = 0;
+        this.interval = setInterval(() => {
+          const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500));
+          if (next <= this.backPosition) {
+            window.scrollTo(0, this.backPosition);
+            clearInterval(this.interval)
+          } else {
+            window.scrollTo(0, next);
+          }
+          i++;
+        }, 16.7)
+      },
+      easeInOutQuad(t, b, c, d) {
+        if ((t /= d / 2) < 1) return c / 2 * t * t + b;
+        return -c / 2 * (--t * (t - 2) - 1) + b;
+      }
+    }
+  }
+</script>
+
+<style scoped>
+  .back-to-top {
+    position: fixed;
+    display: inline-block;
+    text-align: center;
+    cursor: pointer;
+  }
+
+  .back-to-top:hover {
+    background: #d5dbe7;
+  }
+
+  .fade-enter-active,
+  .fade-leave-active {
+    transition: opacity .5s;
+  }
+
+  .fade-enter,
+  .fade-leave-to {
+    opacity: 0
+  }
+
+  .back-to-top .Icon {
+    fill: #9aaabf;
+    background: none;
+  }
+</style>

+ 113 - 0
src/components/Charts/keyboard.vue

@@ -0,0 +1,113 @@
+<template>
+  <div :class="className" :id="id" :style="{height:height,width:width}"></div>
+</template>
+
+<script>
+   import echarts from 'echarts';
+
+   export default {
+     props: {
+       className: {
+         type: String,
+         default: 'chart'
+       },
+       id: {
+         type: String,
+         default: 'chart'
+       },
+       width: {
+         type: String,
+         default: '200px'
+       },
+       height: {
+         type: String,
+         default: '200px'
+       }
+     },
+     data() {
+       return {
+         chart: null
+       };
+     },
+     mounted() {
+       this.initChart();
+     },
+     beforeDestroy() {
+       if (!this.chart) {
+         return
+       }
+       this.chart.dispose();
+       this.chart = null;
+     },
+     methods: {
+       initChart() {
+         this.chart = echarts.init(document.getElementById(this.id));
+
+         const xAxisData = [];
+         const data = [];
+         for (let i = 0; i < 30; i++) {
+           xAxisData.push(i + '号');
+           data.push(Math.round(Math.random() * 2 + 3))
+         }
+
+         this.chart.setOption(
+           {
+             backgroundColor: '#08263a',
+             tooltip: {
+               trigger: 'axis'
+             },
+             xAxis: {
+               show: false,
+               data: xAxisData
+             },
+             visualMap: {
+               show: false,
+               min: 0,
+               max: 50,
+               dimension: 0,
+               inRange: {
+                 color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
+               }
+             },
+             yAxis: {
+               axisLine: {
+                 show: false
+               },
+               axisLabel: {
+                 textStyle: {
+                   color: '#4a657a'
+                 }
+               },
+               splitLine: {
+                 show: true,
+                 lineStyle: {
+                   color: '#08263f'
+                 }
+               },
+               axisTick: {}
+             },
+             series: [{
+               type: 'bar',
+               data,
+               name: '撸文数',
+               itemStyle: {
+                 normal: {
+                   barBorderRadius: 5,
+                   shadowBlur: 10,
+                   shadowColor: '#111'
+                 }
+               },
+               animationEasing: 'elasticOut',
+               animationEasingUpdate: 'elasticOut',
+               animationDelay(idx) {
+                 return idx * 20;
+               },
+               animationDelayUpdate(idx) {
+                 return idx * 20;
+               }
+             }]
+           })
+       }
+     }
+   }
+</script>

+ 150 - 0
src/components/Charts/keyboard2.vue

@@ -0,0 +1,150 @@
+<template>
+  <div :class="className" :id="id" :style="{height:height,width:width}"></div>
+</template>
+
+<script>
+  import echarts from 'echarts';
+
+  export default {
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      id: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '200px'
+      },
+      height: {
+        type: String,
+        default: '200px'
+      }
+    },
+    data() {
+      return {
+        chart: null
+      };
+    },
+    mounted() {
+      this.initChart();
+    },
+    beforeDestroy() {
+      if (!this.chart) {
+        return
+      }
+      this.chart.dispose();
+      this.chart = null;
+    },
+    methods: {
+      initChart() {
+        this.chart = echarts.init(document.getElementById(this.id));
+
+        const xAxisData = [];
+        const data = [];
+        const data2 = [];
+        for (let i = 0; i < 50; i++) {
+          xAxisData.push(i);
+          data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5);
+          data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3);
+        }
+        this.chart.setOption(
+          {
+            backgroundColor: '#08263a',
+            xAxis: [{
+              show: false,
+              data: xAxisData
+            }, {
+              show: false,
+              data: xAxisData
+            }],
+            visualMap: {
+              show: false,
+              min: 0,
+              max: 50,
+              dimension: 0,
+              inRange: {
+                color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
+              }
+            },
+            yAxis: {
+              axisLine: {
+                show: false
+              },
+              axisLabel: {
+                textStyle: {
+                  color: '#4a657a'
+                }
+              },
+              splitLine: {
+                show: true,
+                lineStyle: {
+                  color: '#08263f'
+                }
+              },
+              axisTick: {
+                show: false
+              }
+            },
+            series: [{
+              name: 'back',
+              type: 'bar',
+              data: data2,
+              z: 1,
+              itemStyle: {
+                normal: {
+                  opacity: 0.4,
+                  barBorderRadius: 5,
+                  shadowBlur: 3,
+                  shadowColor: '#111'
+                }
+              }
+            }, {
+              name: 'Simulate Shadow',
+              type: 'line',
+              data,
+              z: 2,
+              showSymbol: false,
+              animationDelay: 0,
+              animationEasing: 'linear',
+              animationDuration: 1200,
+              lineStyle: {
+                normal: {
+                  color: 'transparent'
+                }
+              },
+              areaStyle: {
+                normal: {
+                  color: '#08263a',
+                  shadowBlur: 50,
+                  shadowColor: '#000'
+                }
+              }
+            }, {
+              name: 'front',
+              type: 'bar',
+              data,
+              xAxisIndex: 1,
+              z: 3,
+              itemStyle: {
+                normal: {
+                  barBorderRadius: 5
+                }
+              }
+            }],
+            animationEasing: 'elasticOut',
+            animationEasingUpdate: 'elasticOut',
+            animationDelay(idx) {
+              return idx * 20;
+            },
+            animationDelayUpdate(idx) {
+              return idx * 20;
+            }
+          })
+      }
+    }
+  }
+</script>

+ 222 - 0
src/components/Charts/lineMarker.vue

@@ -0,0 +1,222 @@
+<template>
+  <div :class="className" :id="id" :style="{height:height,width:width}"></div>
+</template>
+
+<script>
+  import echarts from 'echarts';
+
+  export default {
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      id: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '200px'
+      },
+      height: {
+        type: String,
+        default: '200px'
+      }
+    },
+    data() {
+      return {
+        chart: null
+      };
+    },
+    mounted() {
+      this.initChart();
+    },
+    beforeDestroy() {
+      if (!this.chart) {
+        return
+      }
+      this.chart.dispose();
+      this.chart = null;
+    },
+    methods: {
+      initChart() {
+        this.chart = echarts.init(document.getElementById(this.id));
+
+        this.chart.setOption({
+          backgroundColor: '#394056',
+          title: {
+            text: '请求数',
+            textStyle: {
+              fontWeight: 'normal',
+              fontSize: 16,
+              color: '#F1F1F3'
+            },
+            left: '6%'
+          },
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+              lineStyle: {
+                color: '#57617B'
+              }
+            }
+          },
+          legend: {
+            icon: 'rect',
+            itemWidth: 14,
+            itemHeight: 5,
+            itemGap: 13,
+            data: ['移动', '电信', '联通'],
+            right: '4%',
+            textStyle: {
+              fontSize: 12,
+              color: '#F1F1F3'
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            containLabel: true
+          },
+          xAxis: [{
+            type: 'category',
+            boundaryGap: false,
+            axisLine: {
+              lineStyle: {
+                color: '#57617B'
+              }
+            },
+            data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
+          }],
+          yAxis: [{
+            type: 'value',
+            name: '单位(%)',
+            axisTick: {
+              show: false
+            },
+            axisLine: {
+              lineStyle: {
+                color: '#57617B'
+              }
+            },
+            axisLabel: {
+              margin: 10,
+              textStyle: {
+                fontSize: 14
+              }
+            },
+            splitLine: {
+              lineStyle: {
+                color: '#57617B'
+              }
+            }
+          }],
+          series: [{
+            name: '移动',
+            type: 'line',
+            smooth: true,
+            symbol: 'circle',
+            symbolSize: 5,
+            showSymbol: false,
+            lineStyle: {
+              normal: {
+                width: 1
+              }
+            },
+            areaStyle: {
+              normal: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                  offset: 0,
+                  color: 'rgba(137, 189, 27, 0.3)'
+                }, {
+                  offset: 0.8,
+                  color: 'rgba(137, 189, 27, 0)'
+                }], false),
+                shadowColor: 'rgba(0, 0, 0, 0.1)',
+                shadowBlur: 10
+              }
+            },
+            itemStyle: {
+              normal: {
+                color: 'rgb(137,189,27)',
+                borderColor: 'rgba(137,189,2,0.27)',
+                borderWidth: 12
+
+              }
+            },
+            data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
+          }, {
+            name: '电信',
+            type: 'line',
+            smooth: true,
+            symbol: 'circle',
+            symbolSize: 5,
+            showSymbol: false,
+            lineStyle: {
+              normal: {
+                width: 1
+              }
+            },
+            areaStyle: {
+              normal: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                  offset: 0,
+                  color: 'rgba(0, 136, 212, 0.3)'
+                }, {
+                  offset: 0.8,
+                  color: 'rgba(0, 136, 212, 0)'
+                }], false),
+                shadowColor: 'rgba(0, 0, 0, 0.1)',
+                shadowBlur: 10
+              }
+            },
+            itemStyle: {
+              normal: {
+                color: 'rgb(0,136,212)',
+                borderColor: 'rgba(0,136,212,0.2)',
+                borderWidth: 12
+
+              }
+            },
+            data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
+          }, {
+            name: '联通',
+            type: 'line',
+            smooth: true,
+            symbol: 'circle',
+            symbolSize: 5,
+            showSymbol: false,
+            lineStyle: {
+              normal: {
+                width: 1
+              }
+            },
+            areaStyle: {
+              normal: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                  offset: 0,
+                  color: 'rgba(219, 50, 51, 0.3)'
+                }, {
+                  offset: 0.8,
+                  color: 'rgba(219, 50, 51, 0)'
+                }], false),
+                shadowColor: 'rgba(0, 0, 0, 0.1)',
+                shadowBlur: 10
+              }
+            },
+            itemStyle: {
+              normal: {
+                color: 'rgb(219,50,51)',
+                borderColor: 'rgba(219,50,51,0.2)',
+                borderWidth: 12
+              }
+            },
+            data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
+          }]
+        })
+      }
+    }
+}
+</script>

+ 268 - 0
src/components/Charts/mixChart.vue

@@ -0,0 +1,268 @@
+<template>
+  <div :class="className" :id="id" :style="{height:height,width:width}"></div>
+</template>
+
+<script>
+  import echarts from 'echarts';
+
+  export default {
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      id: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '200px'
+      },
+      height: {
+        type: String,
+        default: '200px'
+      }
+    },
+    data() {
+      return {
+        chart: null
+      };
+    },
+    mounted() {
+      this.initChart();
+      this.chart = null;
+    },
+    beforeDestroy() {
+      if (!this.chart) {
+        return
+      }
+      this.chart.dispose();
+      this.chart = null;
+    },
+    methods: {
+      initChart() {
+        this.chart = echarts.init(document.getElementById(this.id));
+        const xData = (function() {
+          const data = [];
+          for (let i = 1; i < 13; i++) {
+            data.push(i + '月份');
+          }
+          return data;
+        }());
+        this.chart.setOption({
+          backgroundColor: '#344b58',
+          title: {
+            text: '统计',
+            x: '4%',
+            textStyle: {
+              color: '#fff',
+              fontSize: '22'
+            },
+            subtextStyle: {
+              color: '#90979c',
+              fontSize: '16'
+            }
+          },
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+              textStyle: {
+                color: '#fff'
+              }
+            }
+          },
+          grid: {
+            borderWidth: 0,
+            top: 110,
+            bottom: 95,
+            textStyle: {
+              color: '#fff'
+            }
+          },
+          legend: {
+            x: '15%',
+            top: '10%',
+            textStyle: {
+              color: '#90979c'
+            },
+            data: ['女', '男', '平均']
+          },
+          calculable: true,
+          xAxis: [{
+            type: 'category',
+            axisLine: {
+              lineStyle: {
+                color: '#90979c'
+              }
+            },
+            splitLine: {
+              show: false
+            },
+            axisTick: {
+              show: false
+            },
+            splitArea: {
+              show: false
+            },
+            axisLabel: {
+              interval: 0
+
+            },
+            data: xData
+          }],
+          yAxis: [{
+            type: 'value',
+            splitLine: {
+              show: false
+            },
+            axisLine: {
+              lineStyle: {
+                color: '#90979c'
+              }
+            },
+            axisTick: {
+              show: false
+            },
+            axisLabel: {
+              interval: 0
+            },
+            splitArea: {
+              show: false
+            }
+          }],
+          dataZoom: [{
+            show: true,
+            height: 30,
+            xAxisIndex: [
+              0
+            ],
+            bottom: 30,
+            start: 10,
+            end: 80,
+            handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
+            handleSize: '110%',
+            handleStyle: {
+              color: '#d3dee5'
+
+            },
+            textStyle: {
+              color: '#fff' },
+            borderColor: '#90979c'
+
+
+          }, {
+            type: 'inside',
+            show: true,
+            height: 15,
+            start: 1,
+            end: 35
+          }],
+          series: [{
+            name: '女',
+            type: 'bar',
+            stack: '总量',
+            barMaxWidth: 35,
+            barGap: '10%',
+            itemStyle: {
+              normal: {
+                color: 'rgba(255,144,128,1)',
+                label: {
+                  show: true,
+                  textStyle: {
+                    color: '#fff'
+                  },
+                  position: 'insideTop',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : '';
+                  }
+                }
+              }
+            },
+            data: [
+              709,
+              1917,
+              2455,
+              2610,
+              1719,
+              1433,
+              1544,
+              3285,
+              5208,
+              3372,
+              2484,
+              4078
+            ]
+          },
+
+          {
+            name: '男',
+            type: 'bar',
+            stack: '总量',
+            itemStyle: {
+              normal: {
+                color: 'rgba(0,191,183,1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : '';
+                  }
+                }
+              }
+            },
+            data: [
+              327,
+              1776,
+              507,
+              1200,
+              800,
+              482,
+              204,
+              1390,
+              1001,
+              951,
+              381,
+              220
+            ]
+          }, {
+            name: '平均',
+            type: 'line',
+            stack: '总量',
+            symbolSize: 10,
+            symbol: 'circle',
+            itemStyle: {
+              normal: {
+                color: 'rgba(252,230,48,1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : '';
+                  }
+                }
+              }
+            },
+            data: [
+              1036,
+              3693,
+              2962,
+              3810,
+              2519,
+              1915,
+              1748,
+              4675,
+              6209,
+              4323,
+              2865,
+              4298
+            ]
+          }
+          ]
+        })
+      }
+    }
+  }
+</script>

+ 296 - 0
src/components/Dropzone/index.vue

@@ -0,0 +1,296 @@
+<template>
+  <div :ref="id" :action="url" class="dropzone" :id="id">
+    <input type="file" name="file">
+  </div>
+</template>
+
+<script>
+  import Dropzone from 'dropzone';
+  import 'dropzone/dist/dropzone.css';
+    // import { getToken } from 'api/qiniu';
+
+  Dropzone.autoDiscover = false;
+
+  export default {
+    data() {
+      return {
+        dropzone: '',
+        initOnce: true
+      }
+    },
+    mounted() {
+      const element = document.getElementById(this.id);
+      const vm = this;
+      this.dropzone = new Dropzone(element, {
+        clickable: this.clickable,
+        thumbnailWidth: this.thumbnailWidth,
+        thumbnailHeight: this.thumbnailHeight,
+        maxFiles: this.maxFiles,
+        maxFilesize: this.maxFilesize,
+        dictRemoveFile: 'Remove',
+        addRemoveLinks: this.showRemoveLink,
+        acceptedFiles: this.acceptedFiles,
+        autoProcessQueue: this.autoProcessQueue,
+        dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
+        dictMaxFilesExceeded: '只能一个图',
+        previewTemplate: '<div class="dz-preview dz-file-preview">  <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div>  <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>  <div class="dz-error-message"><span data-dz-errormessage></span></div>  <div class="dz-success-mark"> <i class="material-icons">done</i> </div>  <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
+        init() {
+          const val = vm.defaultImg;
+          if (!val) return;
+          if (Array.isArray(val)) {
+            if (val.length === 0) return;
+            val.map((v, i) => {
+              const mockFile = { name: 'name' + i, size: 12345, url: v };
+              this.options.addedfile.call(this, mockFile);
+              this.options.thumbnail.call(this, mockFile, v);
+              mockFile.previewElement.classList.add('dz-success');
+              mockFile.previewElement.classList.add('dz-complete');
+              vm.initOnce = false;
+              return true;
+            })
+          } else {
+            const mockFile = { name: 'name', size: 12345, url: val };
+            this.options.addedfile.call(this, mockFile);
+            this.options.thumbnail.call(this, mockFile, val);
+            mockFile.previewElement.classList.add('dz-success');
+            mockFile.previewElement.classList.add('dz-complete');
+            vm.initOnce = false;
+          }
+        },
+        accept: (file, done) => {
+        /* 七牛*/
+            // const token = this.$store.getters.token;
+            // getToken(token).then(response => {
+            //   file.token = response.data.qiniu_token;
+            //   file.key = response.data.qiniu_key;
+            //   file.url = response.data.qiniu_url;
+            //   done();
+            // })
+          done();
+        },
+        sending: (file, xhr, formData) => {
+             /* 七牛*/
+          console.log(file, xhr, formData)
+            // formData.append('token', file.token);
+            // formData.append('key', file.key);
+          vm.initOnce = false;
+        }
+      });
+
+      if (this.couldPaste) {
+        document.addEventListener('paste', this.pasteImg)
+      }
+
+      this.dropzone.on('success', file => {
+        vm.$emit('dropzone-success', file, vm.dropzone.element)
+      });
+      this.dropzone.on('addedfile', file => {
+        vm.$emit('dropzone-fileAdded', file)
+      });
+      this.dropzone.on('removedfile', file => {
+        vm.$emit('dropzone-removedFile', file)
+      });
+      this.dropzone.on('error', (file, error, xhr) => {
+        vm.$emit('dropzone-error', file, error, xhr)
+      });
+      this.dropzone.on('successmultiple', (file, error, xhr) => {
+        vm.$emit('dropzone-successmultiple', file, error, xhr)
+      });
+    },
+    methods: {
+      removeAllFiles() {
+        this.dropzone.removeAllFiles(true)
+      },
+      processQueue() {
+        this.dropzone.processQueue()
+      },
+      pasteImg(event) {
+        const items = (event.clipboardData || event.originalEvent.clipboardData).items;
+        if (items[0].kind === 'file') {
+          this.dropzone.addFile(items[0].getAsFile())
+        }
+      },
+      initImages(val) {
+        if (!val) return;
+        if (Array.isArray(val)) {
+          val.map((v, i) => {
+            const mockFile = { name: 'name' + i, size: 12345, url: v };
+            this.dropzone.options.addedfile.call(this.dropzone, mockFile);
+            this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v);
+            mockFile.previewElement.classList.add('dz-success');
+            mockFile.previewElement.classList.add('dz-complete');
+            return true
+          })
+        } else {
+          const mockFile = { name: 'name', size: 12345, url: val };
+          this.dropzone.options.addedfile.call(this.dropzone, mockFile);
+          this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val);
+          mockFile.previewElement.classList.add('dz-success');
+          mockFile.previewElement.classList.add('dz-complete');
+        }
+      }
+
+    },
+    destroyed() {
+      document.removeEventListener('paste', this.pasteImg);
+      this.dropzone.destroy();
+    },
+    watch: {
+      defaultImg(val) {
+        if (val.length === 0) {
+          this.initOnce = false;
+          return;
+        }
+        if (!this.initOnce) return;
+        this.initImages(val);
+        this.initOnce = false;
+      }
+    },
+    props: {
+      id: {
+        type: String,
+        required: true
+      },
+      url: {
+        type: String,
+        required: true
+      },
+      clickable: {
+        type: Boolean,
+        default: true
+      },
+      defaultMsg: {
+        type: String,
+        default: '上传图片'
+      },
+      acceptedFiles: {
+        type: String
+      },
+      thumbnailHeight: {
+        type: Number,
+        default: 200
+      },
+      thumbnailWidth: {
+        type: Number,
+        default: 200
+      },
+      showRemoveLink: {
+        type: Boolean,
+        default: true
+      },
+      maxFilesize: {
+        type: Number,
+        default: 2
+      },
+      maxFiles: {
+        type: Number,
+        default: 3
+      },
+      autoProcessQueue: {
+        type: Boolean,
+        default: true
+      },
+      useCustomDropzoneOptions: {
+        type: Boolean,
+        default: false
+      },
+      defaultImg: {
+        default: false
+      },
+      couldPaste: {
+        default: false
+      }
+    }
+  }
+</script>
+
+<style scoped>
+    .dropzone {
+        border: 2px solid #E5E5E5;
+        font-family: 'Roboto', sans-serif;
+        color: #777;
+        transition: background-color .2s linear;
+        padding: 5px;
+    }
+
+    .dropzone:hover {
+        background-color: #F6F6F6;
+    }
+
+    i {
+        color: #CCC;
+    }
+
+    .dropzone .dz-image img {
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone input[name='file'] {
+        display: none;
+    }
+
+    .dropzone .dz-preview .dz-image {
+        border-radius: 0px;
+    }
+
+    .dropzone .dz-preview:hover .dz-image img {
+        transform: none;
+        -webkit-filter: none;
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone .dz-preview .dz-details {
+        bottom: 0px;
+        top: 0px;
+        color: white;
+        background-color: rgba(33, 150, 243, 0.8);
+        transition: opacity .2s linear;
+        text-align: left;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
+        background-color: transparent;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:hover span {
+        background-color: transparent;
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-remove {
+        position: absolute;
+        z-index: 30;
+        color: white;
+        margin-left: 15px;
+        padding: 10px;
+        top: inherit;
+        bottom: 15px;
+        border: 2px white solid;
+        text-decoration: none;
+        text-transform: uppercase;
+        font-size: 0.8rem;
+        font-weight: 800;
+        letter-spacing: 1.1px;
+        opacity: 0;
+    }
+
+    .dropzone .dz-preview:hover .dz-remove {
+        opacity: 1;
+    }
+
+    .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
+        margin-left: -40px;
+        margin-top: -50px;
+    }
+
+    .dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
+        color: white;
+        font-size: 5rem;
+    }
+</style>

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

@@ -0,0 +1,56 @@
+<template>
+	<div>
+		<el-badge :is-dot="true" style="line-height: 30px;" @click.native="dialogTableVisible=true">
+			<el-button size="small" type="primary">
+				<svg t="1492682037685" class="bug-svg" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1863"
+				  xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64">
+					<path d="M969.142857 548.571429q0 14.848-10.861714 25.709714t-25.709714 10.861714l-128 0q0 97.718857-38.290286 165.705143l118.857143 119.442286q10.861714 10.861714 10.861714 25.709714t-10.861714 25.709714q-10.276571 10.861714-25.709714 10.861714t-25.709714-10.861714l-113.152-112.566857q-2.852571 2.852571-8.557714 7.424t-23.990857 16.274286-37.156571 20.845714-46.848 16.566857-55.442286 7.424l0-512-73.142857 0 0 512q-29.147429 0-58.002286-7.716571t-49.700571-18.870857-37.705143-22.272-24.868571-18.578286l-8.557714-8.009143-104.557714 118.272q-11.446857 11.995429-27.428571 11.995429-13.714286 0-24.576-9.142857-10.861714-10.276571-11.702857-25.417143t8.850286-26.587429l115.419429-129.718857q-33.133714-65.133714-33.133714-156.562286l-128 0q-14.848 0-25.709714-10.861714t-10.861714-25.709714 10.861714-25.709714 25.709714-10.861714l128 0 0-168.009143-98.852571-98.852571q-10.861714-10.861714-10.861714-25.709714t10.861714-25.709714 25.709714-10.861714 25.709714 10.861714l98.852571 98.852571 482.304 0 98.852571-98.852571q10.861714-10.861714 25.709714-10.861714t25.709714 10.861714 10.861714 25.709714-10.861714 25.709714l-98.852571 98.852571 0 168.009143 128 0q14.848 0 25.709714 10.861714t10.861714 25.709714zM694.857143 219.428571l-365.714286 0q0-75.995429 53.430857-129.426286t129.426286-53.430857 129.426286 53.430857 53.430857 129.426286z"
+					  p-id="1864"></path>
+				</svg>
+			</el-button>
+		</el-badge>
+		<el-dialog title="bug日志" :visible.sync="dialogTableVisible">
+			<el-table :data="logsList">
+				<el-table-column label="message">
+					<template scope="scope">
+						<div>msg:{{ scope.row.err.message }}</div>
+						<br/>
+						<div>url: {{scope.row.url}}</div>
+					</template>
+				</el-table-column>
+				<el-table-column label="stack">
+					<template scope="scope">
+						{{ scope.row.err.stack}}
+					</template>
+				</el-table-column>
+
+			</el-table>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+  export default {
+    name: 'errLog',
+    props: {
+      logsList: {
+        type: Array
+      }
+    },
+    data() {
+      return {
+        dialogTableVisible: false
+      }
+    }
+  }
+</script>
+
+<style scoped>
+.bug-svg {
+	width: 1em;
+	height: 1em;
+	vertical-align: -0.15em;
+	fill: currentColor;
+	overflow: hidden;
+}
+</style>

+ 46 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,46 @@
+<template>
+	<div>
+		<svg t="1492500959545" @click="toggleClick" class="wscn-icon hamburger" :class="{'is-active':isActive}" style="" viewBox="0 0 1024 1024"
+		  version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1691" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64">
+			<path d="M966.8023 568.849776 57.196677 568.849776c-31.397081 0-56.850799-25.452695-56.850799-56.850799l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 543.397081 998.200404 568.849776 966.8023 568.849776z"
+			  p-id="1692"></path>
+			<path d="M966.8023 881.527125 57.196677 881.527125c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 856.07443 998.200404 881.527125 966.8023 881.527125z"
+			  p-id="1693"></path>
+			<path d="M966.8023 256.17345 57.196677 256.17345c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.850799 56.850799-56.850799l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.850799l0 0C1023.653099 230.720755 998.200404 256.17345 966.8023 256.17345z"
+			  p-id="1694"></path>
+		</svg>
+	</div>
+</template>
+
+<script>
+  export default {
+    name: 'hamburger',
+    props: {
+      isActive: {
+        type: Boolean,
+        default: false
+      },
+      toggleClick: {
+        type: Function,
+        default: null
+      }
+    }
+  }
+</script>
+
+<style scoped>
+.hamburger {
+	display: inline-block;
+	cursor: pointer;
+	width: 20px;
+	height: 20px;
+	transform: rotate(0deg);
+	transition: .38s;
+	transform-origin: 50% 50%;
+}
+
+.hamburger.is-active {
+	transform: rotate(90deg);
+}
+
+</style>

+ 22 - 0
src/components/Icon-svg/index.vue

@@ -0,0 +1,22 @@
+<template>
+  <svg class="svg-icon" aria-hidden="true">
+    <use :xlink:href="iconName"></use>
+  </svg>
+</template>
+
+<script>
+  export default {
+    name: 'icon-svg',
+    props: {
+      iconClass: {
+        type: String,
+        required: true
+      }
+    },
+    computed: {
+      iconName() {
+        return `#icon-${this.iconClass}`
+      }
+    }
+  }
+</script>

+ 697 - 0
src/components/ImageCropper/index.vue

@@ -0,0 +1,697 @@
+<template>
+    <div class="vue-image-crop-upload" v-show="show">
+        <div class="vicp-wrap">
+            <div class="vicp-close" @click="off">
+                <i class="vicp-icon4"></i>
+            </div>
+
+            <div class="vicp-step1" v-show="step == 1">
+                <div class="vicp-drop-area"
+                     @dragleave="preventDefault"
+                     @dragover="preventDefault"
+                     @dragenter="preventDefault"
+                     @click="handleClick"
+                     @drop="handleChange">
+                    <i class="vicp-icon1" v-show="loading != 1">
+                        <i class="vicp-icon1-arrow"></i>
+                        <i class="vicp-icon1-body"></i>
+                        <i class="vicp-icon1-bottom"></i>
+                    </i>
+                    <span class="vicp-hint" v-show="loading !== 1">{{ lang.hint }}</span>
+                    <span class="vicp-no-supported-hint" v-show="!isSupported">{{ lang.noSupported }}</span>
+                    <input type="file" v-show="false" @change="handleChange" ref="fileinput">
+                </div>
+                <div class="vicp-error" v-show="hasError">
+                    <i class="vicp-icon2"></i> {{ errorMsg }}
+                </div>
+                <div class="vicp-operate">
+                    <a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
+                </div>
+            </div>
+
+            <div class="vicp-step2" v-if="step == 2">
+                <div class="vicp-crop">
+                    <div class="vicp-crop-left" v-show="true">
+                        <div class="vicp-img-container">
+                            <img :src="sourceImgUrl"
+                                 :style="sourceImgStyle"
+                                 class="vicp-img"
+                                 draggable="false"
+                                 @drag="preventDefault"
+                                 @dragstart="preventDefault"
+                                 @dragend="preventDefault"
+                                 @dragleave="preventDefault"
+                                 @dragover="preventDefault"
+                                 @dragenter="preventDefault"
+                                 @drop="preventDefault"
+                                 @mousedown="imgStartMove"
+                                 @mousemove="imgMove"
+                                 @mouseup="createImg"
+                                 @mouseout="createImg"
+                                 ref="img">
+                            <div class="vicp-img-shade vicp-img-shade-1" :style="sourceImgShadeStyle"></div>
+                            <div class="vicp-img-shade vicp-img-shade-2" :style="sourceImgShadeStyle"></div>
+                        </div>
+                        <div class="vicp-range">
+                            <input type="range" :value="scale.range" step="1" min="0" max="100" @change="zoomChange">
+                            <i @mousedown="startZoomSub" @mouseout="endZoomSub" @mouseup="endZoomSub"
+                               class="vicp-icon5"></i>
+                            <i @mousedown="startZoomAdd" @mouseout="endZoomAdd" @mouseup="endZoomAdd"
+                               class="vicp-icon6"></i>
+                        </div>
+                    </div>
+                    <div class="vicp-crop-right" v-show="true">
+                        <div class="vicp-preview">
+                            <div class="vicp-preview-item">
+                                <img :src="createImgUrl" :style="previewStyle">
+                                <span>{{ lang.preview }}</span>
+                            </div>
+                            <div class="vicp-preview-item">
+                                <img :src="createImgUrl" :style="previewStyle" v-if="!noCircle">
+                                <span>{{ lang.preview }}</span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="vicp-operate">
+                    <a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
+                    <a class="vicp-operate-btn" @click="upload" @mousedown="ripple">{{ lang.btn.save }}</a>
+                </div>
+            </div>
+
+            <div class="vicp-step3" v-if="step == 3">
+                <div class="vicp-upload">
+                    <span class="vicp-loading" v-show="loading === 1">{{ lang.loading }}</span>
+                    <div class="vicp-progress-wrap">
+                        <span class="vicp-progress" v-show="loading === 1" :style="progressStyle"></span>
+                    </div>
+                    <div class="vicp-error" v-show="hasError">
+                        <i class="vicp-icon2"></i> {{ errorMsg }}
+                    </div>
+                    <div class="vicp-success" v-show="loading === 2">
+                        <i class="vicp-icon3"></i> {{ lang.success }}
+                    </div>
+                </div>
+                <div class="vicp-operate">
+                    <a @click="setStep(2)" @mousedown="ripple">{{ lang.btn.back }}</a>
+                    <a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
+                </div>
+            </div>
+            <canvas v-show="false" :width="width" :height="height" ref="canvas"></canvas>
+        </div>
+    </div>
+</template>
+
+<script>
+    /* eslint-disable */
+    import {effectRipple, data2blob} from './utils';
+    import fetch from 'utils/fetch';
+    import langBag from './lang';
+    const mimes = {
+        'jpg': 'image/jpeg',
+        'png': 'image/png',
+        'gif': 'image/gif',
+        'svg': 'image/svg+xml',
+        'psd': 'image/photoshop'
+    };
+
+    export default {
+        props: {
+            // 域,上传文件name,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+            field: {
+                type: String,
+                default: 'avatar'
+            },
+            // 上传地址
+            url: {
+                type: String,
+                default: ''
+            },
+            // 其他要上传文件附带的数据,对象格式
+            params: {
+                type: Object,
+                default: null
+            },
+            // 剪裁图片的宽
+            width: {
+                type: Number,
+                default: 200
+            },
+            // 剪裁图片的高
+            height: {
+                type: Number,
+                default: 200
+            },
+            // 不预览圆形图片
+            noCircle: {
+                type: Boolean,
+                default: false
+            },
+            // 单文件大小限制
+            maxSize: {
+                type: Number,
+                default: 10240
+            },
+            // 语言类型
+            langType: {
+                type: String,
+                'default': 'zh'
+            },
+
+        },
+        data() {
+            let that = this,
+                    {
+                            langType,
+                            width,
+                            height
+                    } = that,
+                    isSupported = true,
+                    lang = langBag[langType] ? langBag[langType] : lang['zh'];
+
+            if (typeof FormData != 'function') {
+                isSupported = false;
+            }
+            return {
+                show: true,
+                // 图片的mime
+                mime:mimes['jpg'],
+                // 语言包
+                lang,
+                // 浏览器是否支持该控件
+                isSupported,
+                // 步骤
+                step: 1, //1选择文件 2剪裁 3上传
+                // 上传状态及进度
+                loading: 0, //0未开始 1正在 2成功 3错误
+                progress: 0,
+                // 是否有错误及错误信息
+                hasError: false,
+                errorMsg: '',
+                // 需求图宽高比
+                ratio: width / height,
+                // 原图地址、生成图片地址
+                sourceImg: null,
+                sourceImgUrl: '',
+                createImgUrl: '',
+                // 原图片拖动事件初始值
+                sourceImgMouseDown: {
+                    on: false,
+                    mX: 0, //鼠标按下的坐标
+                    mY: 0,
+                    x: 0, //scale原图坐标
+                    y: 0
+                },
+                // 生成图片预览的容器大小
+                previewContainer: {
+                    width: 100,
+                    height: 100
+                },
+                // 原图容器宽高
+                sourceImgContainer: { // sic
+                    width: 240,
+                    height: 180
+                },
+                // 原图展示属性
+                scale: {
+                    zoomAddOn: false, //按钮缩放事件开启
+                    zoomSubOn: false, //按钮缩放事件开启
+                    range: 1, //最大100
+                    x: 0,
+                    y: 0,
+                    width: 0,
+                    height: 0,
+                    maxWidth: 0,
+                    maxHeight: 0,
+                    minWidth: 0, //最宽
+                    minHeight: 0,
+                    naturalWidth: 0, //原宽
+                    naturalHeight: 0
+                }
+            }
+        },
+        computed: {
+            // 进度条样式
+            progressStyle() {
+                let {
+                        progress
+                } = this;
+                return {
+                    width: progress + '%'
+                }
+            },
+            // 原图样式
+            sourceImgStyle() {
+                let {
+                        scale,
+                        sourceImgMasking
+                } = this;
+                return {
+                    top: scale.y + sourceImgMasking.y + 'px',
+                    left: scale.x + sourceImgMasking.x + 'px',
+                    width: scale.width + 'px',
+                    height: scale.height + 'px'
+                }
+            },
+            // 原图蒙版属性
+            sourceImgMasking() {
+                let {
+                                width,
+                                height,
+                                ratio,
+                                sourceImgContainer
+                        } = this,
+                        sic = sourceImgContainer,
+                        sicRatio = sic.width / sic.height, // 原图容器宽高比
+                        x = 0,
+                        y = 0,
+                        w = sic.width,
+                        h = sic.height,
+                        scale = 1;
+                if (ratio < sicRatio) {
+                    scale = sic.height / height;
+                    w = sic.height * ratio;
+                    x = (sic.width - w) / 2;
+                }
+                if (ratio > sicRatio) {
+                    scale = sic.width / width;
+                    h = sic.width / ratio;
+                    y = (sic.height - h) / 2;
+                }
+                return {
+                    scale, // 蒙版相对需求宽高的缩放
+                    x,
+                    y,
+                    width: w,
+                    height: h
+                };
+            },
+            // 原图遮罩样式
+            sourceImgShadeStyle() {
+                let sic = this.sourceImgContainer,
+                        sim = this.sourceImgMasking,
+                        w = sim.width == sic.width ? sim.width : (sic.width - sim.width) / 2,
+                        h = sim.height == sic.height ? sim.height : (sic.height - sim.height) / 2;
+                return {
+                    width: w + 'px',
+                    height: h + 'px'
+                };
+            },
+            previewStyle() {
+                let {
+                                width,
+                                height,
+                                ratio,
+                                previewContainer
+                        } = this,
+                        pc = previewContainer,
+                        w = pc.width,
+                        h = pc.height,
+                        pcRatio = w / h;
+                if (ratio < pcRatio) {
+                    w = pc.height * ratio;
+                }
+                if (ratio > pcRatio) {
+                    h = pc.width / ratio;
+                }
+                return {
+                    width: w + 'px',
+                    height: h + 'px'
+                };
+            }
+        },
+        methods: {
+            // 点击波纹效果
+            ripple(e) {
+                effectRipple(e);
+            },
+            // 关闭控件
+            off() {
+                this.show = false;
+                this.$emit('close');
+            },
+            // 设置步骤
+            setStep(step) {
+                let that = this;
+                setTimeout(function () {
+                    that.step = step;
+                }, 200);
+            },
+            /* 图片选择区域函数绑定
+             ---------------------------------------------------------------*/
+            preventDefault(e) {
+                e.preventDefault();
+                return false;
+            },
+            handleClick(e) {
+                if (this.loading !== 1) {
+                    if (e.target !== this.$refs.fileinput) {
+                        e.preventDefault();
+                        if (document.activeElement !== this.$refs) {
+                            this.$refs.fileinput.click();
+                        }
+                    }
+                }
+            },
+            handleChange(e) {
+                e.preventDefault();
+                if (this.loading !== 1) {
+                    let files = e.target.files || e.dataTransfer.files;
+                    this.reset();
+                    if (this.checkFile(files[0])) {
+                        this.setSourceImg(files[0]);
+                    }
+                }
+            },
+            /* ---------------------------------------------------------------*/
+            // 检测选择的文件是否合适
+            checkFile(file) {
+                let that = this,
+                        {
+                                lang,
+                                maxSize
+                        } = that;
+                // 仅限图片
+                if (file.type.indexOf('image') === -1) {
+                    that.hasError = true;
+                    that.errorMsg = lang.error.onlyImg;
+                    return false;
+                }
+                this.mime=file.type;
+                // 超出大小
+                if (file.size / 1024 > maxSize) {
+                    that.hasError = true;
+                    that.errorMsg = lang.error.outOfSize + maxSize + 'kb';
+                    return false;
+                }
+                return true;
+            },
+            // 重置控件
+            reset() {
+                let that = this;
+                that.step = 1;
+                that.loading = 0;
+                that.hasError = false;
+                that.errorMsg = '';
+                that.progress = 0;
+            },
+            // 设置图片源
+            setSourceImg(file) {
+                let that = this,
+                        fr = new FileReader();
+                fr.onload = function (e) {
+                    that.sourceImgUrl = fr.result;
+                    that.startCrop();
+                };
+                fr.readAsDataURL(file);
+            },
+            // 剪裁前准备工作
+            startCrop() {
+                let that = this,
+                        {
+                                width,
+                                height,
+                                ratio,
+                                scale,
+                                sourceImgUrl,
+                                sourceImgMasking,
+                                lang
+                        } = that,
+                        sim = sourceImgMasking,
+                        img = new Image();
+                img.src = sourceImgUrl;
+                img.onload = function () {
+                    let nWidth = img.naturalWidth,
+                            nHeight = img.naturalHeight,
+                            nRatio = nWidth / nHeight,
+                            w = sim.width,
+                            h = sim.height,
+                            x = 0,
+                            y = 0;
+                    // 图片像素不达标
+//                    if (nWidth < width || nHeight < height) {
+//                        that.hasError = true;
+//                        that.errorMsg = lang.error.lowestPx + width + '*' + height;
+//                        return false;
+//                    }
+                    if (ratio > nRatio) {
+                        h = w / nRatio;
+                        y = (sim.height - h) / 2;
+                    }
+                    if (ratio < nRatio) {
+                        w = h * nRatio;
+                        x = (sim.width - w) / 2;
+                    }
+                    scale.range = 0;
+                    scale.x = x;
+                    scale.y = y;
+                    scale.width = w;
+                    scale.height = h;
+                    scale.minWidth = w;
+                    scale.minHeight = h;
+                    scale.maxWidth = nWidth * sim.scale;
+                    scale.maxHeight = nHeight * sim.scale;
+                    scale.naturalWidth = nWidth;
+                    scale.naturalHeight = nHeight;
+                    that.sourceImg = img;
+                    that.createImg();
+                    that.setStep(2);
+                };
+            },
+            // 鼠标按下图片准备移动
+            imgStartMove(e) {
+                let {
+                                sourceImgMouseDown,
+                                scale
+                        } = this,
+                        simd = sourceImgMouseDown;
+                simd.mX = e.screenX;
+                simd.mY = e.screenY;
+                simd.x = scale.x;
+                simd.y = scale.y;
+                simd.on = true;
+            },
+            // 鼠标按下状态下移动,图片移动
+            imgMove(e) {
+                let {
+                                sourceImgMouseDown: {
+                                        on,
+                                        mX,
+                                        mY,
+                                        x,
+                                        y
+                                },
+                                scale,
+                                sourceImgMasking
+                        } = this,
+                        sim = sourceImgMasking,
+                        nX = e.screenX,
+                        nY = e.screenY,
+                        dX = nX - mX,
+                        dY = nY - mY,
+                        rX = x + dX,
+                        rY = y + dY;
+                if (!on) return;
+                if (rX > 0) {
+                    rX = 0;
+                }
+                if (rY > 0) {
+                    rY = 0;
+                }
+                if (rX < sim.width - scale.width) {
+                    rX = sim.width - scale.width;
+                }
+                if (rY < sim.height - scale.height) {
+                    rY = sim.height - scale.height;
+                }
+                scale.x = rX;
+                scale.y = rY;
+            },
+            // 按钮按下开始放大
+            startZoomAdd(e) {
+                let that = this,
+                        {
+                                scale
+                        } = that;
+                scale.zoomAddOn = true;
+                function zoom() {
+                    if (scale.zoomAddOn) {
+                        let range = scale.range >= 100 ? 100 : ++scale.range;
+                        that.zoomImg(range);
+                        setTimeout(function () {
+                            zoom();
+                        }, 60);
+                    }
+                }
+
+                zoom();
+            },
+            // 按钮松开或移开取消放大
+            endZoomAdd(e) {
+                this.scale.zoomAddOn = false;
+            },
+            // 按钮按下开始缩小
+            startZoomSub(e) {
+                let that = this,
+                        {
+                                scale
+                        } = that;
+                scale.zoomSubOn = true;
+                function zoom() {
+                    if (scale.zoomSubOn) {
+                        let range = scale.range <= 0 ? 0 : --scale.range;
+                        that.zoomImg(range);
+                        setTimeout(function () {
+                            zoom();
+                        }, 60);
+                    }
+                }
+
+                zoom();
+            },
+            // 按钮松开或移开取消缩小
+            endZoomSub(e) {
+                let {
+                        scale
+                } = this;
+                scale.zoomSubOn = false;
+            },
+            zoomChange(e) {
+                this.zoomImg(e.target.value);
+            },
+            // 缩放原图
+            zoomImg(newRange) {
+                let that = this,
+                        {
+                                sourceImgMasking,
+                                sourceImgMouseDown,
+                                scale
+                        } = this,
+                        {
+                                maxWidth,
+                                maxHeight,
+                                minWidth,
+                                minHeight,
+                                width,
+                                height,
+                                x,
+                                y,
+                                range
+                        } = scale,
+                        sim = sourceImgMasking,
+                        // 蒙版宽高
+                        sWidth = sim.width,
+                        sHeight = sim.height,
+                        // 新宽高
+                        nWidth = minWidth + (maxWidth - minWidth) * newRange / 100,
+                        nHeight = minHeight + (maxHeight - minHeight) * newRange / 100,
+                        // 新坐标(根据蒙版中心点缩放)
+                        nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x),
+                        nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y);
+                // 判断新坐标是否超过蒙版限制
+                if (nX > 0) {
+                    nX = 0;
+                }
+                if (nY > 0) {
+                    nY = 0;
+                }
+                if (nX < sWidth - nWidth) {
+                    nX = sWidth - nWidth;
+                }
+                if (nY < sHeight - nHeight) {
+                    nY = sHeight - nHeight;
+                }
+                // 赋值处理
+                scale.x = nX;
+                scale.y = nY;
+                scale.width = nWidth;
+                scale.height = nHeight;
+                scale.range = newRange;
+                setTimeout(function () {
+                    if (scale.range == newRange) {
+                        that.createImg();
+                    }
+                }, 300);
+            },
+            // 生成需求图片
+            createImg(e) {
+                let that = this,
+                        {
+                                mime,
+                                sourceImg,
+                                scale: {
+                                        x,
+                                        y,
+                                        width,
+                                        height
+                                },
+                                sourceImgMasking: {
+                                        scale
+                                }
+                        } = that,
+                        canvas = that.$refs.canvas,
+                        ctx = canvas.getContext('2d');
+                if (e) {
+                    // 取消鼠标按下移动状态
+                    that.sourceImgMouseDown.on = false;
+                }
+                ctx.drawImage(sourceImg, x / scale, y / scale, width / scale, height / scale);
+                that.createImgUrl = canvas.toDataURL(mime);
+            },
+            // 上传图片
+            upload() {
+            let that = this,
+                {
+                    lang,
+                    imgFormat,
+                    mime,
+                    url,
+                    params,
+                    headers,
+                    field,
+                    ki,
+                    createImgUrl
+                } = this,
+                fmData = new FormData();
+            fmData.append(field, data2blob(createImgUrl, mime), field + '.' + imgFormat);
+            // 添加其他参数
+            if (typeof params == 'object' && params) {
+                Object.keys(params).forEach((k) => {
+                    fmData.append(k, params[k]);
+                })
+            }
+            // 监听进度回调
+            function uploadProgress (event) {
+                console.log(event)
+                if (event.lengthComputable) {
+                    that.progress = 100 * Math.round(event.loaded) / event.total;
+                }
+            };
+            // 上传文件
+            that.reset();
+            that.loading = 1;
+            that.setStep(3);
+            that.$emit('crop-success', createImgUrl, field, ki);
+            fetch({
+                url,
+                method: 'post',
+                data: fmData
+            }).then(resData=>{
+                that.loading = 2;
+                that.$emit('crop-upload-success', resData.data);
+            }).catch(err=>{
+                if (that.value) {
+                        that.loading = 3;
+                        that.hasError = true;
+                        that.errorMsg = lang.fail;
+                        that.$emit('crop-upload-fail', err, field, ki);
+                    }
+            });
+            }
+        }
+    }
+</script>
+
+<style scoped>
+    @import "./upload.css";
+</style>

+ 41 - 0
src/components/ImageCropper/lang.js

@@ -0,0 +1,41 @@
+const langBag = {
+  zh: {
+    hint: '点击,或拖动图片至此处',
+    loading: '正在上传……',
+    noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
+    success: '上传成功',
+    fail: '图片上传失败',
+    preview: '头像预览',
+    btn: {
+      off: '取消',
+      close: '关闭',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '仅限图片格式',
+      outOfSize: '单文件大小不能超过 ',
+      lowestPx: '图片最低像素为(宽*高):'
+    }
+  },
+  en: {
+    hint: 'Click, or drag the file here',
+    loading: 'Uploading……',
+    noSupported: 'Browser does not support, please use IE10+ or other browsers',
+    success: 'Upload success',
+    fail: 'Upload failed',
+    preview: 'Preview',
+    btn: {
+      off: 'Cancel',
+      close: 'Close',
+      back: 'Back',
+      save: 'Save'
+    },
+    error: {
+      onlyImg: 'Image only',
+      outOfSize: 'Image exceeds size limit: ',
+      lowestPx: 'The lowest pixel in the image: '
+    }
+  }
+};
+export default langBag;

+ 691 - 0
src/components/ImageCropper/upload.css

@@ -0,0 +1,691 @@
+@charset "UTF-8";
+@-webkit-keyframes vicp_progress {
+    0% {
+        background-position-y: 0;
+    }
+    100% {
+        background-position-y: 40px;
+    }
+}
+
+@keyframes vicp_progress {
+    0% {
+        background-position-y: 0;
+    }
+    100% {
+        background-position-y: 40px;
+    }
+}
+
+@-webkit-keyframes vicp {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale(0) translatey(-60px);
+        transform: scale(0) translatey(-60px);
+    }
+    100% {
+        opacity: 1;
+        -webkit-transform: scale(1) translatey(0);
+        transform: scale(1) translatey(0);
+    }
+}
+
+@keyframes vicp {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale(0) translatey(-60px);
+        transform: scale(0) translatey(-60px);
+    }
+    100% {
+        opacity: 1;
+        -webkit-transform: scale(1) translatey(0);
+        transform: scale(1) translatey(0);
+    }
+}
+
+.vue-image-crop-upload {
+    position: fixed;
+    display: block;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    z-index: 10000;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.65);
+    -webkit-tap-highlight-color: transparent;
+    -moz-tap-highlight-color: transparent;
+}
+
+.vue-image-crop-upload .vicp-wrap {
+    -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    position: fixed;
+    display: block;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    z-index: 10000;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    margin: auto;
+    width: 600px;
+    height: 330px;
+    padding: 25px;
+    background-color: #fff;
+    border-radius: 2px;
+    -webkit-animation: vicp 0.12s ease-in;
+    animation: vicp 0.12s ease-in;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-close {
+    position: absolute;
+    right: -30px;
+    top: -30px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4 {
+    position: relative;
+    display: block;
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+    -webkit-transition: -webkit-transform 0.18s;
+    transition: -webkit-transform 0.18s;
+    transition: transform 0.18s;
+    transition: transform 0.18s, -webkit-transform 0.18s;
+    -webkit-transform: rotate(0);
+    -ms-transform: rotate(0);
+    transform: rotate(0);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after, .vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::before {
+    -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    content: '';
+    position: absolute;
+    top: 12px;
+    left: 4px;
+    width: 20px;
+    height: 3px;
+    -webkit-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    transform: rotate(45deg);
+    background-color: #fff;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after {
+    -webkit-transform: rotate(-45deg);
+    -ms-transform: rotate(-45deg);
+    transform: rotate(-45deg);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4:hover {
+    -webkit-transform: rotate(90deg);
+    -ms-transform: rotate(90deg);
+    transform: rotate(90deg);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area {
+    position: relative;
+    padding: 35px;
+    height: 200px;
+    background-color: rgba(0, 0, 0, 0.03);
+    text-align: center;
+    border: 1px dashed rgba(0, 0, 0, 0.08);
+    overflow: hidden;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 {
+    display: block;
+    margin: 0 auto 6px;
+    width: 42px;
+    height: 42px;
+    overflow: hidden;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-arrow {
+    display: block;
+    margin: 0 auto;
+    width: 0;
+    height: 0;
+    border-bottom: 14.7px solid rgba(0, 0, 0, 0.3);
+    border-left: 14.7px solid transparent;
+    border-right: 14.7px solid transparent;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-body {
+    display: block;
+    width: 12.6px;
+    height: 14.7px;
+    margin: 0 auto;
+    background-color: rgba(0, 0, 0, 0.3);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-bottom {
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    display: block;
+    height: 12.6px;
+    border: 6px solid rgba(0, 0, 0, 0.3);
+    border-top: none;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-hint {
+    display: block;
+    padding: 15px;
+    font-size: 14px;
+    color: #666;
+    line-height: 30px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-no-supported-hint {
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    padding: 30px;
+    width: 100%;
+    height: 60px;
+    line-height: 30px;
+    background-color: #eee;
+    text-align: center;
+    color: #666;
+    font-size: 14px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area:hover {
+    cursor: pointer;
+    border-color: rgba(0, 0, 0, 0.1);
+    background-color: rgba(0, 0, 0, 0.05);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop {
+    overflow: hidden;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left {
+    float: left;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container {
+    position: relative;
+    display: block;
+    width: 240px;
+    height: 180px;
+    background-color: #e5e5e0;
+    overflow: hidden;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img {
+    position: absolute;
+    display: block;
+    cursor: move;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img-shade {
+    -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+    position: absolute;
+    background-color: rgba(241, 242, 243, 0.8);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img-shade.vicp-img-shade-1 {
+    top: 0;
+    left: 0;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img-shade.vicp-img-shade-2 {
+    bottom: 0;
+    right: 0;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range {
+    position: relative;
+    margin: 30px 0;
+    width: 240px;
+    height: 18px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5,
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6 {
+    position: absolute;
+    top: 0;
+    width: 18px;
+    height: 18px;
+    border-radius: 100%;
+    background-color: rgba(0, 0, 0, 0.08);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5:hover,
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6:hover {
+    -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+    cursor: pointer;
+    background-color: rgba(0, 0, 0, 0.14);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5 {
+    left: 0;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5::before {
+    position: absolute;
+    content: '';
+    display: block;
+    left: 3px;
+    top: 8px;
+    width: 12px;
+    height: 2px;
+    background-color: #fff;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6 {
+    right: 0;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6::before {
+    position: absolute;
+    content: '';
+    display: block;
+    left: 3px;
+    top: 8px;
+    width: 12px;
+    height: 2px;
+    background-color: #fff;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6::after {
+    position: absolute;
+    content: '';
+    display: block;
+    top: 3px;
+    left: 8px;
+    width: 2px;
+    height: 12px;
+    background-color: #fff;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range] {
+    display: block;
+    padding-top: 5px;
+    margin: 0 auto;
+    width: 180px;
+    height: 8px;
+    vertical-align: top;
+    background: transparent;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    appearance: none;
+    cursor: pointer;
+    /* 滑块
+                     ---------------------------------------------------------------*/
+    /* 轨道
+                     ---------------------------------------------------------------*/
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus {
+    outline: none;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-webkit-slider-thumb {
+    -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+    -webkit-appearance: none;
+    appearance: none;
+    margin-top: -3px;
+    width: 12px;
+    height: 12px;
+    background-color: #61c091;
+    border-radius: 100%;
+    border: none;
+    -webkit-transition: 0.2s;
+    transition: 0.2s;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-moz-range-thumb {
+    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+    -moz-appearance: none;
+    appearance: none;
+    width: 12px;
+    height: 12px;
+    background-color: #61c091;
+    border-radius: 100%;
+    border: none;
+    -webkit-transition: 0.2s;
+    transition: 0.2s;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-thumb {
+    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+    appearance: none;
+    width: 12px;
+    height: 12px;
+    background-color: #61c091;
+    border: none;
+    border-radius: 100%;
+    -webkit-transition: 0.2s;
+    transition: 0.2s;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:active::-moz-range-thumb {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    width: 14px;
+    height: 14px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:active::-ms-thumb {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    width: 14px;
+    height: 14px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:active::-webkit-slider-thumb {
+    -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+    margin-top: -4px;
+    width: 14px;
+    height: 14px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-webkit-slider-runnable-track {
+    -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+    width: 100%;
+    height: 6px;
+    cursor: pointer;
+    border-radius: 2px;
+    border: none;
+    background-color: rgba(68, 170, 119, 0.3);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-moz-range-track {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+    width: 100%;
+    height: 6px;
+    cursor: pointer;
+    border-radius: 2px;
+    border: none;
+    background-color: rgba(68, 170, 119, 0.3);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-track {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+    width: 100%;
+    cursor: pointer;
+    background: transparent;
+    border-color: transparent;
+    color: transparent;
+    height: 6px;
+    border-radius: 2px;
+    border: none;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-fill-lower {
+    background-color: rgba(68, 170, 119, 0.3);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-fill-upper {
+    background-color: rgba(68, 170, 119, 0.15);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-webkit-slider-runnable-track {
+    background-color: rgba(68, 170, 119, 0.5);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-moz-range-track {
+    background-color: rgba(68, 170, 119, 0.5);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-ms-fill-lower {
+    background-color: rgba(68, 170, 119, 0.45);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-ms-fill-upper {
+    background-color: rgba(68, 170, 119, 0.25);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right {
+    float: right;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview {
+    height: 150px;
+    overflow: hidden;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item {
+    position: relative;
+    padding: 5px;
+    width: 100px;
+    height: 100px;
+    float: left;
+    margin-right: 16px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item span {
+    position: absolute;
+    bottom: -30px;
+    width: 100%;
+    font-size: 14px;
+    color: #bbb;
+    display: block;
+    text-align: center;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item img {
+    position: absolute;
+    display: block;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    margin: auto;
+    padding: 3px;
+    background-color: #fff;
+    border: 1px solid rgba(0, 0, 0, 0.15);
+    overflow: hidden;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item:last-child {
+    margin-right: 0;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item:last-child img {
+    border-radius: 100%;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload {
+    position: relative;
+    padding: 35px;
+    height: 200px;
+    background-color: rgba(0, 0, 0, 0.03);
+    text-align: center;
+    border: 1px dashed #ddd;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-loading {
+    display: block;
+    padding: 15px;
+    font-size: 16px;
+    color: #999;
+    line-height: 30px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap {
+    margin-top: 12px;
+    background-color: rgba(0, 0, 0, 0.08);
+    border-radius: 3px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap .vicp-progress {
+    position: relative;
+    display: block;
+    height: 5px;
+    border-radius: 3px;
+    background-color: #4a7;
+    -webkit-box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+    box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+    -webkit-transition: width 0.15s linear;
+    transition: width 0.15s linear;
+    background-image: -webkit-linear-gradient(135deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
+    background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
+    background-size: 40px 40px;
+    -webkit-animation: vicp_progress 0.5s linear infinite;
+    animation: vicp_progress 0.5s linear infinite;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap .vicp-progress::after {
+    content: '';
+    position: absolute;
+    display: block;
+    top: -3px;
+    right: -3px;
+    width: 9px;
+    height: 9px;
+    border: 1px solid rgba(245, 246, 247, 0.7);
+    -webkit-box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+    box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+    border-radius: 100%;
+    background-color: #4a7;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-success {
+    height: 100px;
+    line-height: 100px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-operate {
+    position: absolute;
+    right: 20px;
+    bottom: 20px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-operate a {
+    position: relative;
+    float: left;
+    display: block;
+    margin-left: 10px;
+    width: 100px;
+    height: 36px;
+    line-height: 36px;
+    text-align: center;
+    cursor: pointer;
+    font-size: 14px;
+    color: #4a7;
+    border-radius: 2px;
+    overflow: hidden;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-operate a:hover {
+    background-color: rgba(0, 0, 0, 0.03);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+    display: block;
+    font-size: 14px;
+    line-height: 24px;
+    height: 24px;
+    color: #d10;
+    text-align: center;
+    vertical-align: top;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+    color: #4a7;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-icon3 {
+    position: relative;
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    top: 4px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-icon3::after {
+    position: absolute;
+    top: 3px;
+    left: 6px;
+    width: 6px;
+    height: 10px;
+    border-width: 0 2px 2px 0;
+    border-color: #4a7;
+    border-style: solid;
+    -webkit-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    transform: rotate(45deg);
+    content: '';
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-icon2 {
+    position: relative;
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    top: 4px;
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after, .vue-image-crop-upload .vicp-wrap .vicp-icon2::before {
+    content: '';
+    position: absolute;
+    top: 9px;
+    left: 4px;
+    width: 13px;
+    height: 2px;
+    background-color: #d10;
+    -webkit-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    transform: rotate(45deg);
+}
+
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after {
+    -webkit-transform: rotate(-45deg);
+    -ms-transform: rotate(-45deg);
+    transform: rotate(-45deg);
+}
+
+.e-ripple {
+    position: absolute;
+    border-radius: 100%;
+    background-color: rgba(0, 0, 0, 0.15);
+    background-clip: padding-box;
+    pointer-events: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -webkit-transform: scale(0);
+    -ms-transform: scale(0);
+    transform: scale(0);
+    opacity: 1;
+}
+
+.e-ripple.z-active {
+    opacity: 0;
+    -webkit-transform: scale(2);
+    -ms-transform: scale(2);
+    transform: scale(2);
+    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
+}

+ 58 - 0
src/components/ImageCropper/utils.js

@@ -0,0 +1,58 @@
+/* eslint-disable */
+
+/**
+ *
+ * @param e
+ * @param arg_opts
+ * @returns {boolean}
+ */
+export function effectRipple(e, arg_opts) {
+    let opts = Object.assign({
+            ele: e.target, // 波纹作用元素
+            type: 'hit', // hit点击位置扩散 center中心点扩展
+            bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+        }, arg_opts),
+        target = opts.ele;
+    if (target) {
+        let rect = target.getBoundingClientRect(),
+            ripple = target.querySelector('.e-ripple');
+        if (!ripple) {
+            ripple = document.createElement('span');
+            ripple.className = 'e-ripple';
+            ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px';
+            target.appendChild(ripple);
+        } else {
+            ripple.className = 'e-ripple';
+        }
+        switch (opts.type) {
+            case 'center':
+                ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px';
+                ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px';
+                break;
+            default:
+                ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px';
+                ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px';
+        }
+        ripple.style.backgroundColor = opts.bgc;
+        ripple.className = 'e-ripple z-active';
+        return false;
+    }
+}
+// database64文件格式转换为2进制
+/**
+ *
+ * @param data
+ * @param mime
+ * @returns {*}
+ */
+export function data2blob(data, mime) {
+    // dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
+    data = data.split(',')[1];
+    data = window.atob(data);
+    var ia = new Uint8Array(data.length);
+    for (var i = 0; i < data.length; i++) {
+        ia[i] = data.charCodeAt(i);
+    }
+    // canvas.toDataURL 返回的默认格式就是 image/png
+    return new Blob([ia], {type: mime});
+};

+ 297 - 0
src/components/MDinput/index.vue

@@ -0,0 +1,297 @@
+<template>
+	<div class="material-input__component" :class="computedClasses">
+		<input v-if="type === 'email'" type="email" class="material-input" :name="name" :id="id" :placeholder="placeholder" v-model="valueCopy"
+		  :readonly="readonly" :disabled="disabled" :autocomplete="autocomplete" :required="required" @focus="handleFocus(true)"
+		  @blur="handleFocus(false)" @input="handleModelInput">
+		<input v-if="type === 'url'" type="url" class="material-input" :name="name" :id="id" :placeholder="placeholder" v-model="valueCopy"
+		  :readonly="readonly" :disabled="disabled" :autocomplete="autocomplete" :required="required" @focus="handleFocus(true)"
+		  @blur="handleFocus(false)" @input="handleModelInput">
+		<input v-if="type === 'number'" type="number" class="material-input" :name="name" :id="id" :placeholder="placeholder" v-model="valueCopy"
+		  :readonly="readonly" :disabled="disabled" :autocomplete="autocomplete" :max="max" :min="min" :minlength="minlength" :maxlength="maxlength"
+		  :required="required" @focus="handleFocus(true)" @blur="handleFocus(false)" @input="handleModelInput">
+		<input v-if="type === 'password'" type="password" class="material-input" :name="name" :id="id" :placeholder="placeholder"
+		  v-model="valueCopy" :readonly="readonly" :disabled="disabled" :autocomplete="autocomplete" :max="max" :min="min" :required="required"
+		  @focus="handleFocus(true)" @blur="handleFocus(false)" @input="handleModelInput">
+		<input v-if="type === 'tel'" type="tel" class="material-input" :name="name" :id="id" :placeholder="placeholder" v-model="valueCopy"
+		  :readonly="readonly" :disabled="disabled" :autocomplete="autocomplete" :required="required" @focus="handleFocus(true)"
+		  @blur="handleFocus(false)" @input="handleModelInput">
+		<input v-if="type === 'text'" type="text" class="material-input" :name="name" :id="id" :placeholder="placeholder" v-model="valueCopy"
+		  :readonly="readonly" :disabled="disabled" :autocomplete="autocomplete" :minlength="minlength" :maxlength="maxlength" :required="required"
+		  @focus="handleFocus(true)" @blur="handleFocus(false)" @input="handleModelInput">
+
+		<span class="material-input-bar"></span>
+
+		<label class="material-label">
+            <slot></slot>
+        </label>
+		<div v-if="errorMessages" class="material-errors">
+			<div v-for="error in computedErrors" class="material-error" :key='error'>
+				{{ error }}
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
+	export default {
+  name: 'material-input',
+  computed: {
+    computedErrors() {
+      return typeof this.errorMessages === 'string'
+                        ? [this.errorMessages] : this.errorMessages
+    },
+    computedClasses() {
+      return {
+        'material--active': this.focus,
+        'material--disabled': this.disabled,
+        'material--has-errors': Boolean(!this.valid || (this.errorMessages && this.errorMessages.length)),
+        'material--raised': Boolean(this.focus || this.valueCopy || // has value
+                            (this.placeholder && !this.valueCopy)) // has placeholder
+      }
+    }
+  },
+  data() {
+    return {
+      valueCopy: null,
+      focus: false,
+      valid: true
+    }
+  },
+  beforeMount() {
+        // Here we are following the Vue2 convention on custom v-model:
+        // https://github.com/vuejs/vue/issues/2873#issuecomment-223759341
+    this.copyValue(this.value)
+  },
+  methods: {
+    handleModelInput(event) {
+      this.$emit('input', event.target.value, event)
+      this.handleValidation()
+    },
+    handleFocus(focused) {
+      this.focus = focused
+    },
+    handleValidation() {
+      this.valid = this.$el ? this.$el.querySelector('.material-input').validity.valid : this.valid
+    },
+    copyValue(value) {
+      this.valueCopy = value
+      this.handleValidation()
+    }
+  },
+  watch: {
+    value(newValue) {
+      this.copyValue(newValue)
+    }
+  },
+  props: {
+    id: {
+      type: String,
+      default: null
+    },
+    name: {
+      type: String,
+      default: null
+    },
+    type: {
+      type: String,
+      default: 'text'
+    },
+    value: {
+      default: null
+    },
+    placeholder: {
+      type: String,
+      default: null
+    },
+    readonly: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    min: {
+      type: String,
+      default: null
+    },
+    max: {
+      type: String,
+      default: null
+    },
+    minlength: {
+      type: Number,
+      default: null
+    },
+    maxlength: {
+      type: Number,
+      default: null
+    },
+    required: {
+      type: Boolean,
+      default: true
+    },
+    autocomplete: {
+      type: String,
+      default: 'off'
+    },
+    errorMessages: {
+      type: [Array, String],
+      default: null
+    }
+  }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+    // Fonts:
+    $font-size-base: 16px;
+    $font-size-small: 18px;
+    $font-size-smallest: 12px;
+    $font-weight-normal: normal;
+    // Utils
+    $spacer: 12px;
+    $transition: 0.2s ease all;
+    // Base clases:
+    %base-bar-pseudo {
+        content: '';
+        height: 1px;
+        width: 0;
+        bottom: 0;
+        position: absolute;
+        transition: $transition;
+    }
+
+    // Mixins:
+    @mixin slided-top() {
+        top: -2 * $spacer;
+        font-size: $font-size-small;
+    }
+
+    // Component:
+    .material-input__component {
+        /*margin-top: 30px;*/
+        position: relative;
+        * {
+            box-sizing: border-box;
+        }
+        .material-input {
+            font-size: $font-size-base;
+            padding: $spacer $spacer $spacer $spacer / 2;
+            display: block;
+            width: 100%;
+            border: none;
+            border-radius: 0;
+            &:focus {
+                outline: none;
+                border: none;
+                border-bottom: 1px solid transparent; // fixes the height issue
+            }
+        }
+        .material-label {
+            font-size: $font-size-base;
+            font-weight: $font-weight-normal;
+            position: absolute;
+            pointer-events: none;
+            left: 0;
+            top: $spacer;
+            transition: $transition;
+        }
+        .material-input-bar {
+            position: relative;
+            display: block;
+            width: 100%;
+            &:before {
+                @extend %base-bar-pseudo;
+                left: 50%;
+            }
+            &:after {
+                @extend %base-bar-pseudo;
+                right: 50%;
+            }
+        }
+        // Disabled state:
+        &.material--disabled {
+            .material-input {
+                border-bottom-style: dashed;
+            }
+        }
+        // Raised state:
+        &.material--raised {
+            .material-label {
+                @include slided-top();
+            }
+        }
+        // Active state:
+        &.material--active {
+            .material-input-bar {
+                &:before,
+                &:after {
+                    width: 50%;
+                }
+            }
+        }
+        // Errors:
+        .material-errors {
+            position: relative;
+            overflow: hidden;
+            .material-error {
+                font-size: $font-size-smallest;
+                line-height: $font-size-smallest + 2px;
+                overflow: hidden;
+                margin-top: 0;
+                padding-top: $spacer / 2;
+                padding-right: $spacer / 2;
+                padding-left: 0;
+            }
+        }
+    }
+
+    // Theme:
+    $color-white: white;
+    $color-grey: #9E9E9E;
+    $color-grey-light: #E0E0E0;
+    $color-blue: #2196F3;
+    $color-red: #F44336;
+    $color-black: black;
+    .material-input__component {
+        background: $color-white;
+        .material-input {
+            background: none;
+            color: $color-black;
+            text-indent: 30px;
+            border-bottom: 1px solid $color-grey-light;
+        }
+        .material-label {
+            color: $color-grey;
+        }
+        .material-input-bar {
+            &:before,
+            &:after {
+                background: $color-blue;
+            }
+        }
+        // Active state:
+        &.material--active {
+            .material-label {
+                color: $color-blue;
+            }
+        }
+        // Errors:
+        &.material--has-errors {
+            &.material--active .material-label {
+                color: $color-red;
+            }
+            .material-input-bar {
+                &:before,
+                &:after {
+                    background: $color-red;
+                }
+            }
+            .material-errors {
+                color: $color-red;
+            }
+        }
+    }
+</style>

+ 114 - 0
src/components/MdEditor/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class='simplemde-container' :style="{height:height+'px',zIndex:zIndex}">
+    <textarea :id='id'>
+    </textarea>
+  </div>
+</template>
+
+<script>
+  import 'simplemde/dist/simplemde.min.css';
+  import SimpleMDE from 'simplemde';
+
+  export default {
+    name: 'Sticky',
+    props: {
+      value: String,
+      id: {
+        type: String,
+        default: 'markdown-editor'
+      },
+      autofocus: {
+        type: Boolean,
+        default: false
+      },
+      placeholder: {
+        type: String,
+        default: ''
+      },
+      height: {
+        type: Number,
+        default: 150
+      },
+      zIndex: {
+        type: Number,
+        default: 10
+      },
+      toolbar: {
+        type: Array
+      }
+    },
+    data() {
+      return {
+        simplemde: null,
+        hasChange: false
+      };
+    },
+    watch: {
+      value(val) {
+        if (val === this.simplemde.value() && !this.hasChange) return;
+        this.simplemde.value(val);
+      }
+    },
+    mounted() {
+      this.simplemde = new SimpleMDE({
+        element: document.getElementById(this.id),
+        autofocus: this.autofocus,
+        toolbar: this.toolbar,
+        spellChecker: false,
+        insertTexts: {
+          link: ['[', ']( )']
+        },
+      // hideIcons: ['guide', 'heading', 'quote', 'image', 'preview', 'side-by-side', 'fullscreen'],
+        placeholder: this.placeholder
+      });
+      if (this.value) {
+        this.simplemde.value(this.value);
+      }
+      this.simplemde.codemirror.on('change', () => {
+        if (this.hasChange) {
+          this.hasChange = true
+        }
+        this.$emit('input', this.simplemde.value());
+      });
+    },
+    destroyed() {
+      this.simplemde = null;
+    }
+};
+</script>
+
+<style>
+.simplemde-container .CodeMirror {
+  /*height: 150px;*/
+  min-height: 150px;
+}
+
+.simplemde-container .CodeMirror-scroll {
+  min-height: 150px;
+}
+
+.simplemde-container .CodeMirror-code {
+  padding-bottom: 40px;
+}
+
+.simplemde-container .editor-statusbar {
+  display: none;
+}
+
+.simplemde-container .CodeMirror .CodeMirror-code .cm-link {
+  color: #1482F0;
+}
+
+.simplemde-container .CodeMirror .CodeMirror-code .cm-string.cm-url {
+  color: #2d3b4d;
+  font-weight: bold;
+}
+
+.simplemde-container .CodeMirror .CodeMirror-code .cm-formatting-link-string.cm-url {
+  padding: 0 2px;
+  font-weight: bold;
+  color: #E61E1E;
+}
+</style>
+
+

+ 140 - 0
src/components/PanThumb/index.vue

@@ -0,0 +1,140 @@
+<template>
+	<div class="pan-item" :style="{zIndex:zIndex,height:height,width:width}">
+		<div class="pan-info">
+			<div class="pan-info-roles-container">
+				<slot></slot>
+			</div>
+		</div>
+		<img class="pan-thumb" :src="image">
+	</div>
+</template>
+
+<script>
+  export default {
+    name: 'PanThumb',
+    props: {
+      image: {
+        type: String,
+        required: true
+      },
+      zIndex: {
+        type: Number,
+        default: 100
+      },
+      width: {
+        type: String,
+        default: '150px'
+      },
+      height: {
+        type: String,
+        default: '150px'
+      }
+    }
+  };
+</script>
+
+<style scoped>
+.pan-item {
+	width: 200px;
+	height: 200px;
+	border-radius: 50%;
+	display: inline-block;
+	position: relative;
+	cursor: default;
+	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.pan-info-roles-container {
+	padding: 20px;
+	text-align: center;
+}
+
+.pan-thumb {
+	width: 100%;
+	height: 100%;
+	background-size: 100%;
+	border-radius: 50%;
+	overflow: hidden;
+	position: absolute;
+	transform-origin: 95% 40%;
+	transition: all 0.3s ease-in-out;
+}
+
+.pan-thumb:after {
+	content: '';
+	width: 8px;
+	height: 8px;
+	position: absolute;
+	border-radius: 50%;
+	top: 40%;
+	left: 95%;
+	margin: -4px 0 0 -4px;
+	background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
+	box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
+}
+
+.pan-info {
+	position: absolute;
+	width: inherit;
+	height: inherit;
+	border-radius: 50%;
+	overflow: hidden;
+	box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
+}
+
+.pan-info h3 {
+	color: #fff;
+	text-transform: uppercase;
+	position: relative;
+	letter-spacing: 2px;
+	font-size: 18px;
+	margin: 0 60px;
+	padding: 22px 0 0 0;
+	height: 85px;
+	font-family: 'Open Sans', Arial, sans-serif;
+	text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.pan-info p {
+	color: #fff;
+	padding: 10px 5px;
+	font-style: italic;
+	margin: 0 30px;
+	font-size: 12px;
+	border-top: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+.pan-info p a {
+	display: block;
+	color: #333;
+	width: 80px;
+	height: 80px;
+	background: rgba(255, 255, 255, 0.3);
+	border-radius: 50%;
+	color: #fff;
+	font-style: normal;
+	font-weight: 700;
+	text-transform: uppercase;
+	font-size: 9px;
+	letter-spacing: 1px;
+	padding-top: 24px;
+	margin: 7px auto 0;
+	font-family: 'Open Sans', Arial, sans-serif;
+	opacity: 0;
+	transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
+	transform: translateX(60px) rotate(90deg);
+}
+
+.pan-info p a:hover {
+	background: rgba(255, 255, 255, 0.5);
+}
+
+.pan-item:hover .pan-thumb {
+	transform: rotate(-110deg);
+}
+
+.pan-item:hover .pan-info p a {
+	opacity: 1;
+	transform: translateX(0px) rotate(0deg);
+}
+</style>

+ 53 - 0
src/components/Screenfull/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <svg @click='click' class="icon screenfull" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+    t="1497503607356" viewBox="0 0 1024 1024" version="1.1" p-id="4109" :fill='fill' :width="width" :height="height">
+    <path d="M604.157933 512l204.484208 204.484208 82.942037-82.942037c10.364045-10.952446 26.498514-13.83817 40.309054-8.067746 13.249769 5.742794 22.465664 18.99154 22.465664 33.977859l0 258.042008c0 20.168342-16.695241 36.863582-36.863582 36.863582L659.452283 954.357873c-14.986319 0-28.236088-9.215896-33.977859-23.025413-5.770424-13.249769-2.885723-29.384237 8.067746-39.748283l82.942037-82.942037L512 604.157933 307.515792 808.642141l82.942037 82.942037c10.952446 10.364045 13.83817 26.498514 8.067746 39.748283-5.742794 13.809517-18.99154 23.025413-33.977859 23.025413L106.504686 954.357873c-20.168342 0-36.863582-16.695241-36.863582-36.863582L69.641103 659.452283c0-14.986319 9.215896-28.236088 23.025413-33.977859 13.249769-5.770424 29.384237-2.8847 39.748283 8.067746l82.942037 82.942037 204.484208-204.484208L215.357859 307.515792l-82.942037 82.942037c-6.890944 6.918573-16.10684 10.952446-25.911136 10.952446-4.593622 0-9.804297-1.14815-13.83817-2.8847-13.809517-5.742794-23.025413-18.99154-23.025413-33.977859L69.641103 106.504686c0-20.168342 16.695241-36.863582 36.863582-36.863582L364.546693 69.641103c14.986319 0 28.236088 9.215896 33.977859 23.025413 5.770424 13.249769 2.8847 29.384237-8.067746 39.748283l-82.942037 82.942037 204.484208 204.484208L716.484208 215.357859l-82.942037-82.942037c-10.952446-10.364045-13.83817-26.498514-8.067746-39.748283 5.742794-13.809517 18.99154-23.025413 33.977859-23.025413l258.042008 0c20.168342 0 36.863582 16.695241 36.863582 36.863582l0 258.042008c0 14.986319-9.215896 28.236088-22.465664 33.977859-4.593622 1.736551-9.804297 2.8847-14.397918 2.8847-9.804297 0-19.020192-4.033873-25.911136-10.952446l-82.942037-82.942037L604.157933 512z"
+      p-id="4110" />
+  </svg>
+</template>
+
+<script>
+  import screenfull from 'screenfull';
+  export default {
+    name: 'hamburger',
+    props: {
+      width: {
+        type: Number,
+        default: 22
+      },
+      height: {
+        type: Number,
+        default: 22
+      },
+      fill: {
+        type: String,
+        default: '#48576a'
+      }
+    },
+    data() {
+      return {
+        isFullscreen: false
+      }
+    },
+    methods: {
+      click() {
+        if (!screenfull.enabled) {
+          this.$message({
+            message: 'you browser can not work',
+            type: 'warning'
+          });
+          return false;
+        }
+        screenfull.toggle();
+      }
+    }
+  }
+</script>
+
+<style scoped>
+.screenfull {
+  display: inline-block;
+  cursor: pointer;
+  vertical-align: -0.15em;
+}
+</style>

+ 44 - 0
src/components/SplitPane/Pane.vue

@@ -0,0 +1,44 @@
+<template>
+	<div :class="classes">
+		<slot></slot>
+	</div>
+</template>
+
+<script>
+	export default {
+	  name: 'Pane',
+	  data() {
+	    const classes = ['Pane', this.$parent.split, 'className'];
+    return {
+	      classes: classes.join(' '),
+	      percent: 50
+    }
+  }
+	}
+</script>
+
+<style scoped>
+.splitter-pane.vertical.splitter-paneL {
+    position: absolute;
+    left: 0px;
+    height: 100%;
+}
+
+.splitter-pane.vertical.splitter-paneR {
+    position: absolute;
+    right: 0px;
+    height: 100%;
+}
+
+.splitter-pane.horizontal.splitter-paneL {
+    position: absolute;
+    top: 0px;
+    width: 100%;
+}
+
+.splitter-pane.horizontal.splitter-paneR {
+    position: absolute;
+    bottom: 0px;
+    width: 100%;
+}
+</style>

+ 72 - 0
src/components/SplitPane/Resizer.vue

@@ -0,0 +1,72 @@
+<template>
+	<div :class="classes" @mousedown="onMouseDown"></div>
+</template>
+
+<script>
+  export default {
+    props: {
+      split: {
+        validator(value) {
+          return ['vertical', 'horizontal'].indexOf(value) >= 0
+        },
+        required: true
+      },
+      onMouseDown: {
+        type: Function,
+        required: true
+      }
+    },
+    data() {
+      const classes = ['Resizer', this.split, 'className'];
+      return {
+        classes: classes.join(' ')
+      }
+    }
+  }
+</script>
+
+<style scoped>
+.Resizer {
+	-moz-box-sizing: border-box;
+	-webkit-box-sizing: border-box;
+	box-sizing: border-box;
+	background: #000;
+	position: absolute;
+	opacity: .2;
+	z-index: 1;
+	/*-moz-background-clip: padding;*/
+	/*-webkit-background-clip: padding;*/
+	/*background-clip: padding-box;*/
+}
+/*.Resizer:hover {*/
+/*-webkit-transition: all 2s ease;*/
+/*transition: all 2s ease;*/
+/*}*/
+
+.Resizer.horizontal {
+	height: 11px;
+	margin: -5px 0;
+	border-top: 5px solid rgba(255, 255, 255, 0);
+	border-bottom: 5px solid rgba(255, 255, 255, 0);
+	cursor: row-resize;
+	width: 100%;
+}
+
+.Resizer.horizontal:hover {
+	border-top: 5px solid rgba(0, 0, 0, 0.5);
+	border-bottom: 5px solid rgba(0, 0, 0, 0.5);
+}
+
+.Resizer.vertical {
+	width: 11px;
+	height: 100%;
+	border-left: 5px solid rgba(255, 255, 255, 0);
+	border-right: 5px solid rgba(255, 255, 255, 0);
+	cursor: col-resize;
+}
+
+.Resizer.vertical:hover {
+	border-left: 5px solid rgba(0, 0, 0, 0.5);
+	border-right: 5px solid rgba(0, 0, 0, 0.5);
+}
+</style>

+ 111 - 0
src/components/SplitPane/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <div ref :style="{ cursor, userSelect}" class="vue-splitter-container clearfix" @mouseup="onMouseUp" @mousemove="onMouseMove">
+    <pane class="splitter-pane splitter-paneL" :split="split" :style="{ [type]: percent+'%'}">
+      <slot name="paneL"></slot>
+    </pane>
+    <resizer :style="{ [resizeType]: percent+'%'}" :split="split" :onMouseDown="onMouseDown" @click="onClick"></resizer>
+    <pane class="splitter-pane splitter-paneR" :split="split" :style="{ [type]: 100-percent+'%'}">
+      <slot name="paneR"></slot>
+    </pane>
+  </div>
+</template>
+
+<script>
+  import Resizer from './Resizer';
+  import Pane from './Pane';
+  export default {
+    name: 'splitPane',
+    components: { Resizer, Pane },
+    props: {
+      margin: {
+        type: Number,
+        default: 10
+      },
+      split: {
+        validator(value) {
+          return ['vertical', 'horizontal'].indexOf(value) >= 0
+        },
+        required: true
+      }
+    },
+    data() {
+      return {
+        active: false,
+        hasMoved: false,
+        height: null,
+        percent: 50,
+        type: this.split === 'vertical' ? 'width' : 'height',
+        resizeType: this.split === 'vertical' ? 'left' : 'top'
+      }
+    },
+    computed: {
+      userSelect() {
+        return this.active ? 'none' : ''
+      },
+      cursor() {
+        return this.active ? 'col-resize' : ''
+      }
+    },
+    methods: {
+      onClick() {
+        if (!this.hasMoved) {
+          this.percent = 50;
+          this.$emit('resize');
+        }
+      },
+      onMouseDown() {
+        this.active = true;
+        this.hasMoved = false;
+      },
+      onMouseUp() {
+        this.active = false;
+      },
+      onMouseMove(e) {
+        if (e.buttons === 0 || e.which === 0) {
+          this.active = false;
+        }
+        if (this.active) {
+          let offset = 0;
+          let target = e.currentTarget;
+          if (this.split === 'vertical') {
+            while (target) {
+              offset += target.offsetLeft;
+              target = target.offsetParent;
+            }
+          } else {
+            while (target) {
+              offset += target.offsetTop;
+              target = target.offsetParent;
+            }
+          }
+
+          const currentPage = this.split === 'vertical' ? e.pageX : e.pageY;
+          const targetOffset = this.split === 'vertical' ? e.currentTarget.offsetWidth : e.currentTarget.offsetHeight;
+          const percent = Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100;
+          if (percent > this.margin && percent < 100 - this.margin) {
+            this.percent = percent;
+          }
+          this.$emit('resize');
+          this.hasMoved = true;
+        }
+      }
+    }
+  }
+</script>
+
+<style scoped>
+.clearfix:after {
+  visibility: hidden;
+  display: block;
+  font-size: 0;
+  content: " ";
+  clear: both;
+  height: 0;
+}
+
+.vue-splitter-container {
+  height: 100%;
+  /*display: flex;*/
+  position: relative;
+}
+</style>

+ 74 - 0
src/components/Sticky/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <div :style="{height:height+'px',zIndex:zIndex}">
+    <div :class="className" :style="{top:stickyTop+'px',zIndex:zIndex,position:position,width:width,height:height+'px'}">
+      <slot>
+        <div>sticky</div>
+      </slot>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'Sticky',
+    props: {
+      stickyTop: {
+        type: Number,
+        default: 0
+      },
+      zIndex: {
+        type: Number,
+        default: 1
+      },
+      className: {
+        type: String
+      }
+    },
+    data() {
+      return {
+        active: false,
+        position: '',
+        currentTop: '',
+        width: undefined,
+        height: undefined,
+        child: null,
+        stickyHeight: 0
+
+      };
+    },
+    methods: {
+      sticky() {
+        if (this.active) {
+          return
+        }
+        this.position = 'fixed';
+        this.active = true;
+        this.width = this.width + 'px';
+      },
+      reset() {
+        if (!this.active) {
+          return
+        }
+        this.position = '';
+        this.width = 'auto'
+        this.active = false
+      },
+      handleScroll() {
+        this.width = this.$el.getBoundingClientRect().width;
+        const offsetTop = this.$el.getBoundingClientRect().top;
+        if (offsetTop <= this.stickyTop) {
+          this.sticky();
+          return
+        }
+        this.reset()
+      }
+    },
+    mounted() {
+      this.height = this.$el.getBoundingClientRect().height;
+      window.addEventListener('scroll', this.handleScroll);
+    },
+    destroyed() {
+      window.removeEventListener('scroll', this.handleScroll);
+    }
+  };
+</script>

+ 174 - 0
src/components/Tinymce/index.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class='tinymce-container editor-container'>
+    <textarea class='tinymce-textarea' :id="id"></textarea>
+  </div>
+</template>
+
+<script>
+    // import { getToken, upload } from 'api/qiniu'; // 七牛
+  export default {
+        name: 'tinymce',
+        props: {
+          id: {
+            type: String,
+            default: 'tinymceEditor'
+          },
+          value: {
+            type: String,
+            default: ''
+          },
+          toolbar: {
+            type: Array,
+            required: false,
+            default() {
+              return ['removeformat undo redo |  bullist numlist | outdent indent | forecolor | fullscreen code', 'bold italic blockquote | h2 p  media link | alignleft aligncenter alignright']
+            }
+          },
+          data() {
+            return {
+              hasChange: false,
+              hasInit: false
+            }
+          },
+          menubar: {
+            default: ''
+          },
+          height: {
+            type: Number,
+            required: false,
+            default: 360
+          }
+        },
+        watch: {
+          value(val) {
+            if (!this.hasChange && this.hasInit) {
+              this.$nextTick(() => tinymce.get(this.id).setContent(val))
+            }
+          }
+        },
+        mounted() {
+          const _this = this;
+          tinymce.init({
+            selector: `#${this.id}`,
+            height: this.height,
+            body_class: 'panel-body ',
+            object_resizing: false,
+          //  language: 'zh_CN',
+          //  language_url: '/static/tinymce/langs/zh_CN.js',
+            toolbar: this.toolbar,
+            menubar: this.menubar,
+            plugins: 'advlist,autolink,code,paste,textcolor, colorpicker,fullscreen,link,lists,media,wordcount, imagetools,watermark',
+            end_container_on_empty_block: true,
+            powerpaste_word_import: 'clean',
+            code_dialog_height: 450,
+            code_dialog_width: 1000,
+            advlist_bullet_styles: 'square',
+            advlist_number_styles: 'default',
+            block_formats: '普通标签=p;小标题=h2;',
+            imagetools_cors_hosts: ['wpimg.wallstcn.com', 'wallstreetcn.com'],
+            imagetools_toolbar: 'watermark',
+            default_link_target: '_blank',
+            link_title: false,
+            init_instance_callback: editor => {
+              if (_this.value) {
+                editor.setContent(_this.value)
+              }
+              _this.hasInit = true;
+              editor.on('NodeChange Change KeyUp', () => {
+                this.hasChange = true;
+                this.$emit('input', editor.getContent({ format: 'raw' }));
+              });
+            },
+          // 整合七牛上传
+          // images_dataimg_filter(img) {
+          //   setTimeout(() => {
+          //     const $image = $(img);
+          //     $image.removeAttr('width');
+          //     $image.removeAttr('height');
+          //     if ($image[0].height && $image[0].width) {
+          //       $image.attr('data-wscntype', 'image');
+          //       $image.attr('data-wscnh', $image[0].height);
+          //       $image.attr('data-wscnw', $image[0].width);
+          //       $image.addClass('wscnph');
+          //     }
+          //   }, 0);
+          //   return img
+          // },
+          // images_upload_handler(blobInfo, success, failure, progress) {
+          //   progress(0);
+          //   const token = _this.$store.getters.token;
+          //   getToken(token).then(response => {
+          //     const url = response.data.qiniu_url;
+          //     const formData = new FormData();
+          //     formData.append('token', response.data.qiniu_token);
+          //     formData.append('key', response.data.qiniu_key);
+          //     formData.append('file', blobInfo.blob(), url);
+          //     upload(formData).then(() => {
+          //       success(url);
+          //       progress(100);
+          //     })
+          //   }).catch(err => {
+          //     failure('出现未知问题,刷新页面,或者联系程序员')
+          //     console.log(err);
+          //   });
+          // },
+            setup(editor) {
+              editor.addButton('h2', {
+                title: '小标题', // tooltip text seen on mouseover
+                text: '小标题',
+                onclick() {
+                  editor.execCommand('mceToggleFormat', false, 'h2');
+                },
+                onPostRender() {
+                  const btn = this;
+                  editor.on('init', () => {
+                    editor.formatter.formatChanged('h2', state => {
+                      btn.active(state);
+                    });
+                  });
+                }
+              });
+              editor.addButton('p', {
+                title: '正文',
+                text: '正文',
+                onclick() {
+                  editor.execCommand('mceToggleFormat', false, 'p');
+                },
+                onPostRender() {
+                  const btn = this;
+                  editor.on('init', () => {
+                    editor.formatter.formatChanged('p', state => {
+                      btn.active(state);
+                    });
+                  });
+                }
+              });
+            }
+          });
+        },
+        destroyed() {
+          tinymce.get(this.id).destroy();
+        }
+  }
+</script>
+
+<style scoped>
+.tinymce-container {
+  position: relative
+}
+
+.tinymce-textarea {
+  visibility: hidden;
+  z-index: -1;
+}
+
+.editor-custom-btn-container {
+  position: absolute;
+  right: 15px;
+  top: 18px;
+}
+
+.editor-upload-btn {
+  display: inline-block;
+}
+</style>

+ 70 - 0
src/components/TodoList/Todo.vue

@@ -0,0 +1,70 @@
+<template>
+  <li class="todo" :class="{ completed: todo.done, editing: editing }">
+    <div class="view">
+      <input class="toggle"
+        type="checkbox"
+        :checked="todo.done"
+        @change="toggleTodo( todo)">
+      <label v-text="todo.text" @dblclick="editing = true"></label>
+      <button class="destroy" @click="deleteTodo( todo )"></button>
+    </div>
+    <input class="edit"
+      v-show="editing"
+      v-focus="editing"
+      :value="todo.text"
+      @keyup.enter="doneEdit"
+      @keyup.esc="cancelEdit"
+      @blur="doneEdit">
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'Todo',
+  props: ['todo'],
+  data() {
+    return {
+      editing: false
+    }
+  },
+  directives: {
+    focus(el, { value }, { context }) {
+      if (value) {
+        context.$nextTick(() => {
+          el.focus()
+        })
+      }
+    }
+  },
+  methods: {
+    deleteTodo(todo) {
+      this.$emit('deleteTodo', todo)
+    },
+    editTodo({ todo, value }) {
+      this.$emit('editTodo', { todo, value })
+    },
+    toggleTodo(todo) {
+      this.$emit('toggleTodo', todo)
+    },
+    doneEdit(e) {
+      const value = e.target.value.trim()
+      const { todo } = this
+      if (!value) {
+        this.deleteTodo({
+          todo
+        })
+      } else if (this.editing) {
+        this.editTodo({
+          todo,
+          value
+        })
+        this.editing = false
+      }
+    },
+    cancelEdit(e) {
+      e.target.value = this.todo.text
+      this.editing = false
+    }
+  }
+}
+</script>

+ 318 - 0
src/components/TodoList/index.scss

@@ -0,0 +1,318 @@
+.todoapp {
+	font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+	line-height: 1.4em;
+	color: #4d4d4d;
+	min-width: 230px;
+	max-width: 550px;
+	margin: 40PX auto 0;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	font-weight: 300;
+	button {
+		margin: 0;
+		padding: 0;
+		border: 0;
+		background: none;
+		font-size: 100%;
+		vertical-align: baseline;
+		font-family: inherit;
+		font-weight: inherit;
+		color: inherit;
+		-webkit-appearance: none;
+		appearance: none;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+	}
+	:focus {
+		outline: 0;
+	}
+	.hidden {
+		display: none;
+	}
+	.todoapp {
+		background: #fff;
+		margin: 130px 0 40px 0;
+		position: relative;
+		box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+	}
+	.todoapp input::-webkit-input-placeholder {
+		font-style: italic;
+		font-weight: 300;
+		color: #e6e6e6;
+	}
+	.todoapp input::-moz-placeholder {
+		font-style: italic;
+		font-weight: 300;
+		color: #e6e6e6;
+	}
+	.todoapp input::input-placeholder {
+		font-style: italic;
+		font-weight: 300;
+		color: #e6e6e6;
+	}
+	.todoapp h1 {
+		position: absolute;
+		top: -155px;
+		width: 100%;
+		font-size: 100px;
+		font-weight: 100;
+		text-align: center;
+		color: rgba(175, 47, 47, 0.15);
+		-webkit-text-rendering: optimizeLegibility;
+		-moz-text-rendering: optimizeLegibility;
+		text-rendering: optimizeLegibility;
+	}
+	.new-todo,
+	.edit {
+		position: relative;
+		margin: 0;
+		width: 100%;
+		font-size: 18px;
+		font-family: inherit;
+		font-weight: inherit;
+		line-height: 1.4em;
+		border: 0;
+		color: inherit;
+		padding: 6px;
+		border: 1px solid #999;
+		box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+		box-sizing: border-box;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+	}
+	.new-todo {
+		padding: 16px 16px 16px 60px;
+		border: none;
+		background: rgba(0, 0, 0, 0.003);
+		box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+	}
+	.main {
+		position: relative;
+		z-index: 2;
+		border-top: 1px solid #e6e6e6;
+	}
+	.toggle-all {
+		text-align: center;
+		border: none;
+		/* Mobile Safari */
+		opacity: 0;
+		position: absolute;
+	}
+	.toggle-all+label {
+		width: 60px;
+		height: 34px;
+		font-size: 0;
+		position: absolute;
+		top: -52px;
+		left: -13px;
+		-webkit-transform: rotate(90deg);
+		transform: rotate(90deg);
+	}
+	.toggle-all+label:before {
+		content: '❯';
+		font-size: 22px;
+		color: #e6e6e6;
+		padding: 10px 27px 10px 27px;
+	}
+	.toggle-all:checked+label:before {
+		color: #737373;
+	}
+	.todo-list {
+		margin: 0;
+		padding: 0;
+		list-style: none;
+	}
+	.todo-list li {
+		position: relative;
+		font-size: 24px;
+		border-bottom: 1px solid #ededed;
+	}
+	.todo-list li:last-child {
+		border-bottom: none;
+	}
+	.todo-list li.editing {
+		border-bottom: none;
+		padding: 0;
+	}
+	.todo-list li.editing .edit {
+		display: block;
+		width: 506px;
+		padding: 12px 16px;
+		margin: 0 0 0 43px;
+	}
+	.todo-list li.editing .view {
+		display: none;
+	}
+	.todo-list li .toggle {
+		text-align: center;
+		width: 40px;
+		/* auto, since non-WebKit browsers doesn't support input styling */
+		height: auto;
+		position: absolute;
+		top: 0;
+		bottom: 0;
+		margin: auto 0;
+		border: none;
+		/* Mobile Safari */
+		-webkit-appearance: none;
+		appearance: none;
+	}
+	.todo-list li .toggle {
+		opacity: 0;
+	}
+	.todo-list li .toggle+label {
+		/*
+		Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
+		IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
+	*/
+		background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
+		background-repeat: no-repeat;
+		background-position: center left;
+		background-size: 36px;
+	}
+	.todo-list li .toggle:checked+label {
+		background-size: 36px;
+		background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
+	}
+	.todo-list li label {
+		word-break: break-all;
+		padding: 15px 15px 15px 50px;
+		display: block;
+		line-height: 1.0;
+		    font-size: 14px;
+		transition: color 0.4s;
+	}
+	.todo-list li.completed label {
+		color: #d9d9d9;
+		text-decoration: line-through;
+	}
+	.todo-list li .destroy {
+		display: none;
+		position: absolute;
+		top: 0;
+		right: 10px;
+		bottom: 0;
+		width: 40px;
+		height: 40px;
+		margin: auto 0;
+		font-size: 30px;
+		color: #cc9a9a;
+		transition: color 0.2s ease-out;
+	}
+	.todo-list li .destroy:hover {
+		color: #af5b5e;
+	}
+	.todo-list li .destroy:after {
+		content: '×';
+	}
+	.todo-list li:hover .destroy {
+		display: block;
+	}
+	.todo-list li .edit {
+		display: none;
+	}
+	.todo-list li.editing:last-child {
+		margin-bottom: -1px;
+	}
+	.footer {
+		color: #777;
+		position: relative;
+		padding: 10px 15px;
+		height: 40px;
+		text-align: center;
+		border-top: 1px solid #e6e6e6;
+	}
+	.footer:before {
+		content: '';
+		position: absolute;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		height: 50px;
+		overflow: hidden;
+		box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+	}
+	.todo-count {
+		float: left;
+		text-align: left;
+	}
+	.todo-count strong {
+		font-weight: 300;
+	}
+	.filters {
+		margin: 0;
+		padding: 0;
+		list-style: none;
+		position: absolute;
+		right: 0;
+		left: -20px;
+	}
+	.filters li {
+		display: inline;
+	}
+	.filters li a {
+		color: inherit;
+		margin: 3px;
+		font-size: 12px;
+		padding: 3px 7px;
+		text-decoration: none;
+		border: 1px solid transparent;
+		border-radius: 3px;
+	}
+	.filters li a:hover {
+		border-color: rgba(175, 47, 47, 0.1);
+	}
+	.filters li a.selected {
+		border-color: rgba(175, 47, 47, 0.2);
+	}
+	.clear-completed,
+	html .clear-completed:active {
+		float: right;
+		position: relative;
+		line-height: 20px;
+		text-decoration: none;
+		cursor: pointer;
+	}
+	.clear-completed:hover {
+		text-decoration: underline;
+	}
+	.info {
+		margin: 65px auto 0;
+		color: #bfbfbf;
+		font-size: 10px;
+		text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+		text-align: center;
+	}
+	.info p {
+		line-height: 1;
+	}
+	.info a {
+		color: inherit;
+		text-decoration: none;
+		font-weight: 400;
+	}
+	.info a:hover {
+		text-decoration: underline;
+	}
+	/*
+	Hack to remove background from Mobile Safari.
+	Can't use it globally since it destroys checkboxes in Firefox
+*/
+	@media screen and (-webkit-min-device-pixel-ratio:0) {
+		.toggle-all,
+		.todo-list li .toggle {
+			background: none;
+		}
+		.todo-list li .toggle {
+			height: 40px;
+		}
+	}
+	@media (max-width: 430px) {
+		.footer {
+			height: 50px;
+		}
+		.filters {
+			bottom: 10px;
+		}
+	}
+}

+ 116 - 0
src/components/TodoList/index.vue

@@ -0,0 +1,116 @@
+<template>
+  <section class="todoapp">
+    <!-- header -->
+    <header class="header">
+      <input class="new-todo" autofocus autocomplete="off" placeholder="TODO LIST?" @keyup.enter="addTodo">
+    </header>
+    <!-- main section -->
+    <section class="main" v-show="todos.length">
+      <input class="toggle-all" id="toggle-all" type="checkbox" :checked="allChecked" @change="toggleAll({ done: !allChecked })">
+      <label for="toggle-all"></label>
+      <ul class="todo-list">
+        <todo @toggleTodo='toggleTodo' @editTodo='editTodo' @deleteTodo='deleteTodo' v-for="(todo, index) in filteredTodos" :key="index"
+          :todo="todo"></todo>
+      </ul>
+    </section>
+    <!-- footer -->
+    <footer class="footer" v-show="todos.length">
+      <span class="todo-count">
+        <strong>{{ remaining }}</strong>
+        {{ remaining | pluralize('item') }} left
+      </span>
+      <ul class="filters">
+        <li v-for="(val, key) in filters" :key="key">
+          <a :class="{ selected: visibility === key }" @click.prevent="visibility = key">{{ key | capitalize }}</a>
+        </li>
+      </ul>
+      <button class="clear-completed" v-show="todos.length > remaining" @click="clearCompleted">
+        Clear completed
+      </button>
+    </footer>
+  </section>
+</template>
+
+<script>
+import Todo from './Todo.vue'
+const STORAGE_KEY = 'todos'
+const filters = {
+  all: todos => todos,
+  active: todos => todos.filter(todo => !todo.done),
+  completed: todos => todos.filter(todo => todo.done)
+}
+const defalutList = [
+  { text: 'star this repository', done: false },
+  { text: 'fork this repository', done: false },
+  { text: 'follow author', done: false }
+]
+export default {
+  components: { Todo },
+  data() {
+    return {
+      visibility: 'all',
+      filters,
+      todos: JSON.parse(window.localStorage.getItem(STORAGE_KEY)) || defalutList
+    }
+  },
+  computed: {
+    allChecked() {
+      return this.todos.every(todo => todo.done)
+    },
+    filteredTodos() {
+      return filters[this.visibility](this.todos)
+    },
+    remaining() {
+      return this.todos.filter(todo => !todo.done).length
+    }
+  },
+  methods: {
+    setLocalStorgae() {
+      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos))
+    },
+    addTodo(e) {
+      const text = e.target.value
+      if (text.trim()) {
+        this.todos.push({
+          text,
+          done: false
+        })
+        this.setLocalStorgae()
+      }
+      e.target.value = ''
+    },
+    toggleTodo(val) {
+      val.done = !val.done
+      this.setLocalStorgae()
+    },
+    deleteTodo(todo) {
+      console.log(todo)
+      this.todos.splice(this.todos.indexOf(todo), 1)
+      this.setLocalStorgae()
+    },
+    editTodo({ todo, value }) {
+      console.log(todo, value)
+      todo.text = value
+      this.setLocalStorgae()
+    },
+    clearCompleted() {
+      this.todos = this.todos.filter(todo => !todo.done)
+      this.setLocalStorgae()
+    },
+    toggleAll({ done }) {
+      this.todos.forEach(todo => {
+        todo.done = done
+        this.setLocalStorgae()
+      })
+    }
+  },
+  filters: {
+    pluralize: (n, w) => n === 1 ? w : w + 's',
+    capitalize: s => s.charAt(0).toUpperCase() + s.slice(1)
+  }
+}
+</script>
+
+<style lang="scss">
+  @import './index.scss';
+</style>

+ 122 - 0
src/components/Upload/singleImage.vue

@@ -0,0 +1,122 @@
+<template>
+    <div class="upload-container">
+        <el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post"
+            :on-success="handleImageScucess">
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        </el-upload>
+        <div class="image-preview">
+            <div class="image-preview-wrapper" v-show="imageUrl.length>1">
+                <img :src="imageUrl+'?imageView2/1/w/200/h/200'">
+                <div class="image-preview-action">
+                    <i @click="rmImage" class="el-icon-delete"></i>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+	// 预览效果见付费文章
+	import { getToken } from 'api/qiniu';
+	export default {
+	  name: 'singleImageUpload',
+	  props: {
+	    value: String
+  },
+	  computed: {
+	    imageUrl() {
+	      return this.value
+    }
+  },
+	  data() {
+	    return {
+	      tempUrl: '',
+	      dataObj: { token: '', key: '' }
+    };
+  },
+	  methods: {
+	    rmImage() {
+	      this.emitInput('');
+    },
+	    emitInput(val) {
+	      this.$emit('input', val);
+    },
+	    handleImageScucess() {
+	      this.emitInput(this.tempUrl)
+    },
+	    beforeUpload() {
+	      const _self = this;
+	      return new Promise((resolve, reject) => {
+	        getToken().then(response => {
+	          const key = response.data.qiniu_key;
+	          const token = response.data.qiniu_token;
+	          _self._data.dataObj.token = token;
+	          _self._data.dataObj.key = key;
+	          this.tempUrl = response.data.qiniu_url;
+	          resolve(true);
+        }).catch(err => {
+	          console.log(err);
+	          reject(false)
+        });
+      });
+    }
+  }
+	};
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+    @import "src/styles/mixin.scss";
+    .upload-container {
+        width: 100%;
+        position: relative;
+        @include clearfix;
+        .image-uploader {
+            width: 60%;
+            float: left;
+        }
+        .image-preview {
+            width: 200px;
+            height: 200px;
+            position: relative;
+            border: 1px dashed #d9d9d9;
+            float: left;
+            margin-left: 50px;
+            .image-preview-wrapper {
+                position: relative;
+                width: 100%;
+                height: 100%;
+                img {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+            .image-preview-action {
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                left: 0;
+                top: 0;
+                cursor: default;
+                text-align: center;
+                color: #fff;
+                opacity: 0;
+                font-size: 20px;
+                background-color: rgba(0, 0, 0, .5);
+                transition: opacity .3s;
+                cursor: pointer;
+                text-align: center;
+                line-height: 200px;
+                .el-icon-delete {
+                    font-size: 36px;
+                }
+            }
+            &:hover {
+                .image-preview-action {
+                    opacity: 1;
+                }
+            }
+        }
+    }
+
+</style>

+ 119 - 0
src/components/Upload/singleImage2.vue

@@ -0,0 +1,119 @@
+<template>
+	<div class="singleImageUpload2 upload-container">
+		<el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post"
+		  :on-success="handleImageScucess">
+			<i class="el-icon-upload"></i>
+			<div class="el-upload__text">Drag或<em>点击上传</em></div>
+		</el-upload>
+		<div v-show="imageUrl.length>0" class="image-preview">
+			<div class="image-preview-wrapper" v-show="imageUrl.length>1">
+				<img :src="imageUrl">
+				<div class="image-preview-action">
+					<i @click="rmImage" class="el-icon-delete"></i>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	// 预览效果见专题
+	import { getToken } from 'api/qiniu';
+	export default {
+	  name: 'singleImageUpload2',
+	  props: {
+	    value: String
+  },
+	  computed: {
+	    imageUrl() {
+	      return this.value
+    }
+  },
+	  data() {
+	    return {
+	      tempUrl: '',
+	      dataObj: { token: '', key: '' }
+    };
+  },
+	  methods: {
+	    rmImage() {
+	      this.emitInput('');
+    },
+	    emitInput(val) {
+	      this.$emit('input', val);
+    },
+	    handleImageScucess() {
+	      this.emitInput(this.tempUrl)
+    },
+	    beforeUpload() {
+	      const _self = this;
+	      return new Promise((resolve, reject) => {
+	        getToken().then(response => {
+	          const key = response.data.qiniu_key;
+	          const token = response.data.qiniu_token;
+	          _self._data.dataObj.token = token;
+	          _self._data.dataObj.key = key;
+	          this.tempUrl = response.data.qiniu_url;
+	          resolve(true);
+        }).catch(err => {
+	          console.log(err);
+	          reject(false)
+        });
+      });
+    }
+  }
+	};
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.upload-container {
+	width: 100%;
+	height: 100%;
+	position: relative;
+	.image-uploader {
+		height: 100%;
+	}
+	.image-preview {
+		width: 100%;
+		height: 100%;
+		position: absolute;
+		left: 0px;
+		top: 0px;
+		border: 1px dashed #d9d9d9;
+		.image-preview-wrapper {
+			position: relative;
+			width: 100%;
+			height: 100%;
+			img {
+				width: 100%;
+				height: 100%;
+			}
+		}
+		.image-preview-action {
+			position: absolute;
+			width: 100%;
+			height: 100%;
+			left: 0;
+			top: 0;
+			cursor: default;
+			text-align: center;
+			color: #fff;
+			opacity: 0;
+			font-size: 20px;
+			background-color: rgba(0, 0, 0, .5);
+			transition: opacity .3s;
+			cursor: pointer;
+			text-align: center;
+			line-height: 200px;
+			.el-icon-delete {
+				font-size: 36px;
+			}
+		}
+		&:hover {
+			.image-preview-action {
+				opacity: 1;
+			}
+		}
+	}
+}
+</style>

+ 146 - 0
src/components/Upload/singleImage3.vue

@@ -0,0 +1,146 @@
+<template>
+	<div class="upload-container">
+		<el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post"
+		  :on-success="handleImageScucess">
+			<i class="el-icon-upload"></i>
+			<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+		</el-upload>
+		<div class="image-preview image-app-preview">
+			<div class="image-preview-wrapper" v-show="imageUrl.length>1">
+				<div class='app-fake-conver'>&nbsp&nbsp全球 付费节目单 最热 经济</div>
+				<img :src="imageUrl">
+				<div class="image-preview-action">
+					<i @click="rmImage" class="el-icon-delete"></i>
+				</div>
+			</div>
+		</div>
+		<div class="image-preview">
+			<div class="image-preview-wrapper" v-show="imageUrl.length>1">
+				<img :src="imageUrl">
+				<div class="image-preview-action">
+					<i @click="rmImage" class="el-icon-delete"></i>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	// 预览效果见文章
+	import { getToken } from 'api/qiniu';
+	export default {
+	  name: 'singleImageUpload',
+	  props: {
+	    value: String
+  },
+	  computed: {
+	    imageUrl() {
+	      return this.value
+    }
+  },
+	  data() {
+	    return {
+	      tempUrl: '',
+	      dataObj: { token: '', key: '' }
+    };
+  },
+	  methods: {
+	    rmImage() {
+	      this.emitInput('');
+    },
+	    emitInput(val) {
+	      this.$emit('input', val);
+    },
+	    handleImageScucess(file) {
+	      this.emitInput(file.files.file)
+    },
+	    beforeUpload() {
+	      const _self = this;
+	      return new Promise((resolve, reject) => {
+	        getToken().then(response => {
+	          const key = response.data.qiniu_key;
+	          const token = response.data.qiniu_token;
+	          _self._data.dataObj.token = token;
+	          _self._data.dataObj.key = key;
+	          this.tempUrl = response.data.qiniu_url;
+	          resolve(true);
+        }).catch(err => {
+	          console.log(err);
+	          reject(false)
+        });
+      });
+    }
+  }
+	};
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+@import "src/styles/mixin.scss";
+.upload-container {
+	width: 100%;
+	position: relative;
+	@include clearfix;
+	.image-uploader {
+		width: 35%;
+		float: left;
+	}
+	.image-preview {
+		width: 200px;
+		height: 200px;
+		position: relative;
+		border: 1px dashed #d9d9d9;
+		float: left;
+		margin-left: 50px;
+		.image-preview-wrapper {
+			position: relative;
+			width: 100%;
+			height: 100%;
+			img {
+				width: 100%;
+				height: 100%;
+			}
+		}
+		.image-preview-action {
+			position: absolute;
+			width: 100%;
+			height: 100%;
+			left: 0;
+			top: 0;
+			cursor: default;
+			text-align: center;
+			color: #fff;
+			opacity: 0;
+			font-size: 20px;
+			background-color: rgba(0, 0, 0, .5);
+			transition: opacity .3s;
+			cursor: pointer;
+			text-align: center;
+			line-height: 200px;
+			.el-icon-delete {
+				font-size: 36px;
+			}
+		}
+		&:hover {
+			.image-preview-action {
+				opacity: 1;
+			}
+		}
+	}
+	.image-app-preview {
+		width: 320px;
+		height: 180px;
+		position: relative;
+		border: 1px dashed #d9d9d9;
+		float: left;
+		margin-left: 50px;
+		.app-fake-conver {
+			height: 44px;
+			position: absolute;
+			width: 100%; // background: rgba(0, 0, 0, .1);
+			text-align: center;
+			line-height: 64px;
+			color: #fff;
+		}
+	}
+}
+</style>

+ 64 - 0
src/components/jsonEditor/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class='json-editor'>
+    <textarea ref='textarea'></textarea>
+  </div>
+</template>
+
+<script>
+  import CodeMirror from 'codemirror';
+  import 'codemirror/addon/lint/lint.css';
+  import 'codemirror/lib/codemirror.css';
+  import 'codemirror/theme/rubyblue.css';
+  require('script-loader!jsonlint');
+  import 'codemirror/mode/javascript/javascript'
+  import 'codemirror/addon/lint/lint'
+  import 'codemirror/addon/lint/json-lint';
+
+  export default {
+    name: 'jsonEditor',
+    data() {
+      return {
+        jsonEditor: false
+      }
+    },
+    props: ['value'],
+    watch: {
+      value(value) {
+        const editor_value = this.jsonEditor.getValue();
+        if (value !== editor_value) {
+          this.jsonEditor.setValue(JSON.stringify(this.value, null, 2));
+        }
+      }
+    },
+    mounted() {
+      this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
+        lineNumbers: true,
+        mode: 'application/json',
+        gutters: ['CodeMirror-lint-markers'],
+        theme: 'rubyblue',
+        lint: true
+      });
+
+      this.jsonEditor.setValue(JSON.stringify(this.value, null, 2));
+      this.jsonEditor.on('change', cm => {
+        this.$emit('changed', cm.getValue())
+        this.$emit('input', cm.getValue())
+      })
+    },
+    methods: {
+      getValue() {
+        return this.jsonEditor.getValue()
+      }
+    }
+  }
+</script>
+
+<style>
+.CodeMirror {
+  height: 100%;
+}
+
+.json-editor .cm-s-rubyblue span.cm-string {
+  color: #F08047;
+}
+</style>

+ 158 - 0
src/components/twoDndList/index.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="twoDndList">
+    <div class="twoDndList-list" :style="{width:width1}">
+      <h3>{{list1Title}}</h3>
+      <draggable :list="list1" class="dragArea" :options="{group:'article'}">
+        <div class="list-complete-item" v-for="element in list1" :key='element'>
+          <div class="list-complete-item-handle">[{{element.author}}] {{element.title}}</div>
+          <div style="position:absolute;right:0px;">
+            <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
+                        <i style="color:#ff4949" class="el-icon-delete"></i>
+                    </span>
+          </div>
+        </div>
+      </draggable>
+    </div>
+
+    <div class="twoDndList-list" :style="{width:width2}">
+      <h3>{{list2Title}}</h3>
+      <draggable :list="filterList2" class="dragArea" :options="{group:'article'}">
+        <div class="list-complete-item" v-for="element in filterList2" :key='element'>
+          <div class='list-complete-item-handle2' @click="pushEle(element)"> [{{element.author}}] {{element.title}}</div>
+        </div>
+      </draggable>
+    </div>
+  </div>
+</template>
+
+<script>
+  import draggable from 'vuedraggable'
+  export default {
+    name: 'twoDndList',
+    components: { draggable },
+    computed: {
+      filterList2() {
+        return this.list2.filter(v => {
+          if (this.isNotInList1(v)) {
+            return v
+          }
+          return false;
+        })
+      }
+    },
+    props: {
+      list1: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      list2: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      list1Title: {
+        type: String,
+        default: 'list1'
+      },
+      list2Title: {
+        type: String,
+        default: 'list2'
+      },
+      width1: {
+        type: String,
+        default: '48%'
+      },
+      width2: {
+        type: String,
+        default: '48%'
+      }
+    },
+    methods: {
+      isNotInList1(v) {
+        return this.list1.every(k => v.id !== k.id)
+      },
+      isNotInList2(v) {
+        return this.list2.every(k => v.id !== k.id)
+      },
+      deleteEle(ele) {
+        for (const item of this.list1) {
+          if (item.id === ele.id) {
+            const index = this.list1.indexOf(item);
+            this.list1.splice(index, 1)
+            break
+          }
+        }
+        if (this.isNotInList2(ele)) {
+          this.list2.unshift(ele)
+        }
+      },
+      pushEle(ele) {
+        this.list1.push(ele)
+      }
+    }
+  }
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.twoDndList {
+  background: #fff;
+  padding-bottom: 40px;
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+  .twoDndList-list {
+    float: left;
+    padding-bottom: 30px;
+    &:first-of-type {
+      margin-right: 2%;
+    }
+    .dragArea {
+      margin-top: 15px;
+      min-height: 50px;
+      padding-bottom: 30px;
+    }
+  }
+}
+
+.list-complete-item {
+  cursor: pointer;
+  position: relative;
+  font-size: 14px;
+  padding: 5px 12px;
+  margin-top: 4px;
+  border: 1px solid #bfcbd9;
+  transition: all 1s;
+}
+
+.list-complete-item-handle {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 50px;
+}
+
+.list-complete-item-handle2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 20px;
+}
+
+.list-complete-item.sortable-chosen {
+  background: #4AB7BD;
+}
+
+.list-complete-item.sortable-ghost {
+  background: #30B08F;
+}
+
+.list-complete-enter,
+.list-complete-leave-active {
+  opacity: 0;
+}
+</style>

+ 91 - 0
src/directive/sticky.js

@@ -0,0 +1,91 @@
+const vueSticky = {};
+let listenAction;
+vueSticky.install = Vue => {
+  Vue.directive('sticky', {
+    inserted(el, binding) {
+      const params = binding.value || {},
+        stickyTop = params.stickyTop || 0,
+        zIndex = params.zIndex || 1000,
+        elStyle = el.style;
+
+      elStyle.position = '-webkit-sticky';
+      elStyle.position = 'sticky';
+        // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
+        // if (~elStyle.position.indexOf('sticky')) {
+        //     elStyle.top = `${stickyTop}px`;
+        //     elStyle.zIndex = zIndex;
+        //     return
+        // }
+      const elHeight = el.getBoundingClientRect().height;
+      const elWidth = el.getBoundingClientRect().width;
+      elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`;
+
+      const parentElm = el.parentNode || document.documentElement;
+      const placeholder = document.createElement('div');
+      placeholder.style.display = 'none';
+      placeholder.style.width = `${elWidth}px`;
+      placeholder.style.height = `${elHeight}px`;
+      parentElm.insertBefore(placeholder, el)
+
+      let active = false;
+
+      const getScroll = (target, top) => {
+        const prop = top ? 'pageYOffset' : 'pageXOffset';
+        const method = top ? 'scrollTop' : 'scrollLeft';
+        let ret = target[prop];
+        if (typeof ret !== 'number') {
+          ret = window.document.documentElement[method];
+        }
+        return ret;
+      };
+
+      const sticky = () => {
+        if (active) {
+          return
+        }
+        if (!elStyle.height) {
+          elStyle.height = `${el.offsetHeight}px`
+        }
+
+        elStyle.position = 'fixed';
+        elStyle.width = `${elWidth}px`;
+        placeholder.style.display = 'inline-block';
+        active = true
+      };
+
+      const reset = () => {
+        if (!active) {
+          return
+        }
+
+        elStyle.position = '';
+        placeholder.style.display = 'none';
+        active = false;
+      };
+
+      const check = () => {
+        const scrollTop = getScroll(window, true);
+        const offsetTop = el.getBoundingClientRect().top;
+        if (offsetTop < stickyTop) {
+          sticky();
+        } else {
+          if (scrollTop < elHeight + stickyTop) {
+            reset()
+          }
+        }
+      };
+      listenAction = () => {
+        check()
+      };
+
+      window.addEventListener('scroll', listenAction)
+    },
+
+    unbind() {
+      window.removeEventListener('scroll', listenAction)
+    }
+  })
+};
+
+export default vueSticky
+

+ 26 - 0
src/directive/waves.css

@@ -0,0 +1,26 @@
+.waves-ripple {
+    position: absolute;
+    border-radius: 100%;
+    background-color: rgba(0, 0, 0, 0.15);
+    background-clip: padding-box;
+    pointer-events: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -webkit-transform: scale(0);
+    -ms-transform: scale(0);
+    transform: scale(0);
+    opacity: 1;
+}
+
+.waves-ripple.z-active {
+    opacity: 0;
+    -webkit-transform: scale(2);
+    -ms-transform: scale(2);
+    transform: scale(2);
+    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
+}

+ 47 - 0
src/directive/waves.js

@@ -0,0 +1,47 @@
+import './waves.css';
+
+const vueWaves = {};
+vueWaves.install = (Vue, options = {}) => {
+  Vue.directive('waves', {
+    bind(el, binding) {
+      el.addEventListener('click', e => {
+        const customOpts = Object.assign(options, binding.value);
+        const opts = Object.assign({
+            ele: el, // 波纹作用元素
+            type: 'hit', // hit点击位置扩散center中心点扩展
+            color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+          }, customOpts),
+          target = opts.ele;
+        if (target) {
+          target.style.position = 'relative';
+          target.style.overflow = 'hidden';
+          const rect = target.getBoundingClientRect();
+          let ripple = target.querySelector('.waves-ripple');
+          if (!ripple) {
+            ripple = document.createElement('span');
+            ripple.className = 'waves-ripple';
+            ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px';
+            target.appendChild(ripple);
+          } else {
+            ripple.className = 'waves-ripple';
+          }
+          switch (opts.type) {
+            case 'center':
+              ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px';
+              ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px';
+              break;
+            default:
+              ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px';
+              ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px';
+          }
+          ripple.style.backgroundColor = opts.color;
+          ripple.className = 'waves-ripple z-active';
+          return false;
+        }
+      }, false);
+    }
+  })
+};
+
+export default vueWaves;
+

+ 103 - 0
src/filters/index.js

@@ -0,0 +1,103 @@
+function pluralize(time, label) {
+  if (time === 1) {
+    return time + label
+  }
+  return time + label + 's'
+}
+export function timeAgo(time) {
+  const between = Date.now() / 1000 - Number(time);
+  if (between < 3600) {
+    return pluralize(~~(between / 60), ' minute')
+  } else if (between < 86400) {
+    return pluralize(~~(between / 3600), ' hour')
+  } else {
+    return pluralize(~~(between / 86400), ' day')
+  }
+}
+
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0) {
+    return null;
+  }
+
+  if ((time + '').length === 10) {
+    time = +time * 1000
+  }
+
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}';
+  let date;
+  if (typeof time == 'object') {
+    date = time;
+  } else {
+    date = new Date(parseInt(time));
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  };
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key];
+    if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1];
+    if (result.length > 0 && value < 10) {
+      value = '0' + value;
+    }
+    return value || 0;
+  });
+  return time_str;
+}
+
+export function formatTime(time, option) {
+  time = +time * 1000;
+  const d = new Date(time);
+  const now = Date.now();
+
+  const diff = (now - d) / 1000;
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) { // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
+  }
+}
+
+/* 数字 格式化*/
+export function nFormatter(num, digits) {
+  const si = [
+        { value: 1E18, symbol: 'E' },
+        { value: 1E15, symbol: 'P' },
+        { value: 1E12, symbol: 'T' },
+        { value: 1E9, symbol: 'G' },
+        { value: 1E6, symbol: 'M' },
+        { value: 1E3, symbol: 'k' }
+  ];
+  for (let i = 0; i < si.length; i++) {
+    if (num >= si[i].value) {
+      return (num / si[i].value + 0.1).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol;
+    }
+  }
+  return num.toString();
+}
+
+export function html2Text(val) {
+  const div = document.createElement('div');
+  div.innerHTML = val;
+  return div.textContent || div.innerText;
+}
+
+export function toThousandslsFilter(num) {
+  return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','));
+}

+ 109 - 0
src/main.js

@@ -0,0 +1,109 @@
+// The Vue build version to load with the `import` command
+// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
+import Vue from 'vue';
+import App from './App';
+import router from './router';
+import store from './store';
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-default/index.css';
+import 'assets/custom-theme/index.css'; // 换肤版本element-ui css
+import NProgress from 'nprogress'; // Progress 进度条
+import 'nprogress/nprogress.css';// Progress 进度条 样式
+import 'normalize.css/normalize.css';// normalize.css 样式格式化
+import 'assets/iconfont/iconfont'; // iconfont 具体图标见https://github.com/PanJiaChen/vue-element-admin/wiki
+import * as filters from './filters'; // 全局vue filter
+import Multiselect from 'vue-multiselect';// 使用的一个多选框组件,element-ui的select不能满足所有需求
+import 'vue-multiselect/dist/vue-multiselect.min.css';// 多选框组件css
+import Sticky from 'components/Sticky'; // 粘性header组件
+import IconSvg from 'components/Icon-svg';// svg 组件
+import vueWaves from './directive/waves';// 水波纹指令
+import errLog from 'store/errLog';// error log组件
+import './mock/index.js';  // 该项目所有请求使用mockjs模拟
+import { getToken } from 'utils/auth';
+
+// register globally
+Vue.component('multiselect', Multiselect);
+Vue.component('Sticky', Sticky);
+Vue.component('icon-svg', IconSvg)
+Vue.use(ElementUI);
+Vue.use(vueWaves);
+
+// register global utility filters.
+Object.keys(filters).forEach(key => {
+  Vue.filter(key, filters[key])
+});
+
+// permissiom judge
+function hasPermission(roles, permissionRoles) {
+  if (roles.indexOf('admin') >= 0) return true; // admin权限 直接通过
+  if (!permissionRoles) return true;
+  return roles.some(role => permissionRoles.indexOf(role) >= 0)
+}
+
+// register global progress.
+const whiteList = ['/login', '/authredirect', '/reset', '/sendpwd'];// 不重定向白名单
+router.beforeEach((to, from, next) => {
+  NProgress.start(); // 开启Progress
+  if (getToken()) { // 判断是否有token
+    if (to.path === '/login') {
+      next({ path: '/' });
+    } else {
+      if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
+        store.dispatch('GetInfo').then(res => { // 拉取user_info
+          const roles = res.data.role;
+          store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
+            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
+            next({ ...to }); // hack方法 确保addRoutes已完成
+          })
+        }).catch(() => {
+          store.dispatch('FedLogOut').then(() => {
+            next({ path: '/login' });
+          })
+        })
+      } else {
+        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
+        if (hasPermission(store.getters.roles, to.meta.role)) {
+          next();//
+        } else {
+          next({ path: '/401', query: { noGoBack: true } });
+        }
+        // 可删 ↑
+      }
+    }
+  } else {
+    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
+      next()
+    } else {
+      next('/login'); // 否则全部重定向到登录页
+      NProgress.done(); // 在hash模式下 改变手动改变hash 重定向回来 不会触发afterEach 暂时hack方案 ps:history模式下无问题,可删除该行!
+    }
+  }
+});
+
+router.afterEach(() => {
+  NProgress.done(); // 结束Progress
+});
+
+Vue.config.productionTip = false;
+
+// 生产环境错误日志
+if (process.env.NODE_ENV === 'production') {
+  Vue.config.errorHandler = function(err, vm) {
+    console.log(err, window.location.href);
+    errLog.pushLog({
+      err,
+      url: window.location.href,
+      vm
+    })
+  };
+}
+
+new Vue({
+  el: '#app',
+  router,
+  store,
+  template: '<App/>',
+  components: { App }
+})
+
+

+ 37 - 0
src/mock/article.js

@@ -0,0 +1,37 @@
+import Mock from 'mockjs';
+
+
+const List = [];
+const count = 20;
+
+
+for (let i = 0; i < count; i++) {
+  List.push(Mock.mock({
+    id: '@id',
+    title: '@ctitle(10, 20)',
+    'status|1': ['published', 'draft'],
+    author: '@cname',
+    display_time: '@datetime',
+    pageviews: '@integer(300, 5000)'
+  }));
+}
+
+export default {
+  getList: () => List,
+  getArticle: () => ({
+    id: 120000000001,
+    author: { key: 'mockPan' },
+    source_name: '原创作者',
+    category_item: [{ key: 'global', name: '全球' }],
+    comment_disabled: false,
+    content: '<p>我是测试数据我是测试数据</p><p><img class="wscnph" src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943" data-wscntype="image" data-wscnh="300" data-wscnw="400" data-mce-src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>"',
+    content_short: '我是测试数据',
+    display_time: +new Date(),
+    image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
+    platforms: ['a-platform'],
+    source_uri: 'https://github.com/PanJiaChen/vue-element-admin',
+    status: 'published',
+    tags: [],
+    title: ''
+  })
+};

+ 44 - 0
src/mock/article_table.js

@@ -0,0 +1,44 @@
+import Mock from 'mockjs';
+import { param2Obj } from 'utils';
+
+const List = [];
+const count = 100;
+
+for (let i = 0; i < count; i++) {
+  List.push(Mock.mock({
+    id: '@increment',
+    timestamp: +Mock.Random.date('T'),
+    author: '@cname',
+    auditor: '@cname',
+    title: '@ctitle(10, 20)',
+    forecast: '@float(0, 100, 2, 2)',
+    importance: '@integer(1, 3)',
+    'type|1': ['CN', 'US', 'JP', 'EU'],
+    'status|1': ['published', 'draft', 'deleted'],
+    pageviews: '@integer(300, 5000)'
+  }));
+}
+
+export default {
+  getList: config => {
+    const { importance, type, title, page, limit, sort } = param2Obj(config.url);
+    let mockList = List.filter(item => {
+      if (importance && item.importance !== +importance) return false;
+      if (type && item.type !== type) return false;
+      if (title && item.title.indexOf(title) < 0) return false;
+      return true;
+    });
+    if (sort === '-id') {
+      mockList = mockList.reverse()
+    }
+
+    const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1));
+    return {
+      total: mockList.length,
+      items: pageList
+    }
+  },
+  getPv: () => ({
+    pvData: [{ key: 'PC网站', pv: 1024 }, { key: 'mobile网站', pv: 1024 }, { key: 'ios', pv: 1024 }, { key: 'android', pv: 1024 }]
+  })
+};

+ 25 - 0
src/mock/index.js

@@ -0,0 +1,25 @@
+import Mock from 'mockjs';
+import loginAPI from './login';
+import articleAPI from './article';
+import article_tableAPI from './article_table';
+import remoteSearchAPI from './remoteSearch';
+
+
+// 登录相关
+Mock.mock(/\/login\/loginbyemail/, 'post', loginAPI.loginByEmail);
+Mock.mock(/\/login\/logout/, 'post', loginAPI.logout);
+Mock.mock(/\/user\/info\.*/, 'get', loginAPI.getInfo)
+
+// // 文章相关
+Mock.mock(/\/article\/list/, 'get', articleAPI.getList);
+Mock.mock(/\/article\/detail/, 'get', articleAPI.getArticle);
+
+// // table example相关
+Mock.mock(/\/article_table\/list/, 'get', article_tableAPI.getList);
+Mock.mock(/\/article_table\/p/, 'get', article_tableAPI.getPv);
+
+// // 搜索相关
+Mock.mock(/\/search\/user/, 'get', remoteSearchAPI.searchUser);
+
+
+export default Mock;

+ 41 - 0
src/mock/login.js

@@ -0,0 +1,41 @@
+import { param2Obj } from 'utils';
+
+const userMap = {
+  admin: {
+    role: ['admin'],
+    token: 'admin',
+    introduction: '我是超级管理员',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  editor: {
+    role: ['editor'],
+    token: 'editor',
+    introduction: '我是编辑',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  },
+  developer: {
+    role: ['develop'],
+    token: 'develop',
+    introduction: '我是开发',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: '工程师小王'
+  }
+}
+
+export default {
+  loginByEmail: config => {
+    const { email } = JSON.parse(config.body);
+    return userMap[email.split('@')[0]];
+  },
+  getInfo: config => {
+    const { token } = param2Obj(config.url);
+    if (userMap[token]) {
+      return userMap[token];
+    } else {
+      return Promise.reject('a');
+    }
+  },
+  logout: () => 'success'
+};

+ 24 - 0
src/mock/remoteSearch.js

@@ -0,0 +1,24 @@
+import Mock from 'mockjs';
+import { param2Obj } from 'utils';
+
+const NameList = [];
+const count = 100;
+
+for (let i = 0; i < count; i++) {
+  NameList.push(Mock.mock({
+    name: '@first'
+  }));
+}
+NameList.push({ name: 'mockPan' })
+
+export default {
+  searchUser: config => {
+    const { name } = param2Obj(config.url);
+    const mockNameList = NameList.filter(item => {
+      const lowerCaseName = item.name.toLowerCase()
+      if (name && lowerCaseName.indexOf(name.toLowerCase()) < 0) return false;
+      return true;
+    });
+    return { items: mockNameList }
+  }
+};

+ 1 - 0
src/router/_import_development.js

@@ -0,0 +1 @@
+module.exports = file => require('@/views/' + file + '.vue')

+ 1 - 0
src/router/_import_production.js

@@ -0,0 +1 @@
+module.exports = file => () => import('@/views/' + file + '.vue')

+ 161 - 0
src/router/index.js

@@ -0,0 +1,161 @@
+import Vue from 'vue';
+import Router from 'vue-router';
+const _import = require('./_import_' + process.env.NODE_ENV);
+// in development env not use Lazy Loading,because Lazy Loading large page will cause webpack hot update too slow.so only in production use Lazy Loading
+
+Vue.use(Router);
+
+/* layout */
+import Layout from '../views/layout/Layout';
+
+/**
+* icon : the icon show in the sidebar
+* hidden : if `hidden:true` will not show in the sidebar
+* redirect : if `redirect:noredirect` will no redirct in the levelbar
+* noDropdown : if `noDropdown:true` will has no submenu
+* meta : { role: ['admin'] }  will control the page role
+**/
+export const constantRouterMap = [
+    { path: '/login', component: _import('login/index'), hidden: true },
+    { path: '/authredirect', component: _import('login/authredirect'), hidden: true },
+    { path: '/404', component: _import('error/404'), hidden: true },
+    { path: '/401', component: _import('error/401'), hidden: true },
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/dashboard',
+    name: '首页',
+    hidden: true,
+    children: [{ path: 'dashboard', component: _import('dashboard/index') }]
+  },
+  {
+    path: '/introduction',
+    component: Layout,
+    redirect: '/introduction/index',
+    icon: 'xinrenzhinan',
+    noDropdown: true,
+    children: [{ path: 'index', component: _import('introduction/index'), name: '简述' }]
+  }
+]
+
+export default new Router({
+  // mode: 'history', //后端支持可开
+  scrollBehavior: () => ({ y: 0 }),
+  routes: constantRouterMap
+});
+
+export const asyncRouterMap = [
+  {
+    path: '/permission',
+    component: Layout,
+    redirect: '/permission/index',
+    name: '权限测试',
+    icon: 'quanxian',
+    meta: { role: ['admin'] },
+    noDropdown: true,
+    children: [{ path: 'index', component: _import('permission/index'), name: '权限测试页', meta: { role: ['admin'] } }]
+  },
+  {
+    path: '/components',
+    component: Layout,
+    redirect: '/components/index',
+    name: '组件',
+    icon: 'zujian',
+    children: [
+      { path: 'index', component: _import('components/index'), name: '介绍 ' },
+      { path: 'tinymce', component: _import('components/tinymce'), name: '富文本编辑器' },
+      { path: 'markdown', component: _import('components/markdown'), name: 'Markdown' },
+      { path: 'jsoneditor', component: _import('components/jsoneditor'), name: 'JSON编辑器' },
+      { path: 'dndlist', component: _import('components/dndlist'), name: '列表拖拽' },
+      { path: 'splitpane', component: _import('components/splitpane'), name: 'SplitPane' },
+      { path: 'avatarupload', component: _import('components/avatarUpload'), name: '头像上传' },
+      { path: 'dropzone', component: _import('components/dropzone'), name: 'Dropzone' },
+      { path: 'sticky', component: _import('components/sticky'), name: 'Sticky' },
+      { path: 'countto', component: _import('components/countTo'), name: 'CountTo' },
+      { path: 'mixin', component: _import('components/mixin'), name: '小组件' },
+      { path: 'backtotop', component: _import('components/backToTop'), name: '返回顶部' }
+    ]
+  },
+  {
+    path: '/charts',
+    component: Layout,
+    redirect: '/charts/index',
+    name: '图表',
+    icon: 'tubiaoleixingzhengchang',
+    children: [
+      { path: 'index', component: _import('charts/index'), name: '介绍' },
+      { path: 'keyboard', component: _import('charts/keyboard'), name: '键盘图表' },
+      { path: 'keyboard2', component: _import('charts/keyboard2'), name: '键盘图表2' },
+      { path: 'line', component: _import('charts/line'), name: '折线图' },
+      { path: 'mixchart', component: _import('charts/mixChart'), name: '混合图表' }
+    ]
+  },
+  {
+    path: '/example',
+    component: Layout,
+    redirect: 'noredirect',
+    name: '综合实例',
+    icon: 'zonghe',
+    children: [
+      {
+        path: '/example/table',
+        component: _import('example/table/index'),
+        redirect: '/example/table/table',
+        name: 'Table',
+        icon: 'table',
+        children: [
+          { path: 'dynamictable', component: _import('example/table/dynamictable'), name: '动态table' },
+          { path: 'dragtable', component: _import('example/table/dragTable'), name: '拖拽table' },
+          { path: 'inline_edit_table', component: _import('example/table/inlineEditTable'), name: 'table内编辑' },
+          { path: 'table', component: _import('example/table/table'), name: '综合table' }
+        ]
+      },
+      { path: 'form/edit', icon: 'ziliaoshouce', component: _import('example/form'), name: '编辑Form', meta: { isEdit: true } },
+      { path: 'form/create', icon: 'yinhangqia', component: _import('example/form'), name: '创建Form' },
+
+      { path: 'tab/index', icon: 'mobankuangjia', component: _import('example/tab/index'), name: 'Tab' }
+    ]
+  },
+  {
+    path: '/errorpage',
+    component: Layout,
+    redirect: 'noredirect',
+    name: '错误页面',
+    icon: '404',
+    children: [
+      { path: '401', component: _import('error/401'), name: '401' },
+      { path: '404', component: _import('error/404'), name: '404' }
+    ]
+  },
+  {
+    path: '/errlog',
+    component: Layout,
+    redirect: 'noredirect',
+    name: 'errlog',
+    icon: 'bug',
+    noDropdown: true,
+    children: [{ path: 'log', component: _import('errlog/index'), name: '错误日志' }]
+  },
+  {
+    path: '/excel',
+    component: Layout,
+    redirect: 'noredirect',
+    name: 'excel',
+    icon: 'EXCEL',
+    children: [
+      { path: 'download', component: _import('excel/index'), name: '导出excel' },
+      { path: 'download2', component: _import('excel/selectExcel'), name: '选择导出excel' }
+    ]
+  },
+  {
+    path: '/theme',
+    component: Layout,
+    redirect: 'noredirect',
+    name: 'theme',
+    icon: 'theme',
+    noDropdown: true,
+    children: [{ path: 'index', component: _import('theme/index'), name: '换肤' }]
+  },
+
+  { path: '*', redirect: '/404', hidden: true }
+];

+ 13 - 0
src/store/errLog.js

@@ -0,0 +1,13 @@
+const errLog = {
+  state: {
+    errLog: []
+  },
+  pushLog(log) {
+    this.state.errLog.unshift(log)
+  },
+  clearLog() {
+    this.state.errLog = [];
+  }
+};
+
+export default errLog;

+ 14 - 0
src/store/getters.js

@@ -0,0 +1,14 @@
+const getters = {
+  sidebar: state => state.app.sidebar,
+  visitedViews: state => state.app.visitedViews,
+  token: state => state.user.token,
+  avatar: state => state.user.avatar,
+  name: state => state.user.name,
+  introduction: state => state.user.introduction,
+  status: state => state.user.status,
+  roles: state => state.user.roles,
+  setting: state => state.user.setting,
+  permission_routers: state => state.permission.routers,
+  addRouters: state => state.permission.addRouters
+};
+export default getters

+ 19 - 0
src/store/index.js

@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import app from './modules/app';
+import user from './modules/user';
+import permission from './modules/permission';
+import getters from './getters';
+
+Vue.use(Vuex);
+
+const store = new Vuex.Store({
+  modules: {
+    app,
+    user,
+    permission
+  },
+  getters
+});
+
+export default store

+ 49 - 0
src/store/modules/app.js

@@ -0,0 +1,49 @@
+import Cookies from 'js-cookie';
+
+const app = {
+  state: {
+    sidebar: {
+      opened: !+Cookies.get('sidebarStatus')
+    },
+    theme: 'default',
+    livenewsChannels: Cookies.get('livenewsChannels') || '[]',
+    visitedViews: []
+  },
+  mutations: {
+    TOGGLE_SIDEBAR: state => {
+      if (state.sidebar.opened) {
+        Cookies.set('sidebarStatus', 1);
+      } else {
+        Cookies.set('sidebarStatus', 0);
+      }
+      state.sidebar.opened = !state.sidebar.opened;
+    },
+    ADD_VISITED_VIEWS: (state, view) => {
+      if (state.visitedViews.some(v => v.path === view.path)) return
+      state.visitedViews.push({ name: view.name, path: view.path })
+    },
+    DEL_VISITED_VIEWS: (state, view) => {
+      let index
+      for (const [i, v] of state.visitedViews.entries()) {
+        if (v.path === view.path) {
+          index = i
+          break
+        }
+      }
+      state.visitedViews.splice(index, 1)
+    }
+  },
+  actions: {
+    ToggleSideBar: ({ commit }) => {
+      commit('TOGGLE_SIDEBAR')
+    },
+    addVisitedViews: ({ commit }, view) => {
+      commit('ADD_VISITED_VIEWS', view)
+    },
+    delVisitedViews: ({ commit }, view) => {
+      commit('DEL_VISITED_VIEWS', view)
+    }
+  }
+};
+
+export default app;

+ 62 - 0
src/store/modules/permission.js

@@ -0,0 +1,62 @@
+import { asyncRouterMap, constantRouterMap } from 'src/router'
+
+/**
+ * 通过meta.role判断是否与当前用户权限匹配
+ * @param roles
+ * @param route
+ */
+function hasPermission(roles, route) {
+  if (route.meta && route.meta.role) {
+    return roles.some(role => route.meta.role.indexOf(role) >= 0)
+  } else {
+    return true
+  }
+}
+
+/**
+ * 递归过滤异步路由表,返回符合用户角色权限的路由表
+ * @param asyncRouterMap
+ * @param roles
+ */
+function filterAsyncRouter(asyncRouterMap, roles) {
+  const accessedRouters = asyncRouterMap.filter(route => {
+    if (hasPermission(roles, route)) {
+      if (route.children && route.children.length) {
+        route.children = filterAsyncRouter(route.children, roles)
+      }
+      return true
+    }
+    return false
+  })
+  return accessedRouters
+}
+
+const permission = {
+  state: {
+    routers: constantRouterMap,
+    addRouters: []
+  },
+  mutations: {
+    SET_ROUTERS: (state, routers) => {
+      state.addRouters = routers
+      state.routers = constantRouterMap.concat(routers)
+    }
+  },
+  actions: {
+    GenerateRoutes({ commit }, data) {
+      return new Promise(resolve => {
+        const { roles } = data
+        let accessedRouters
+        if (roles.indexOf('admin') >= 0) {
+          accessedRouters = asyncRouterMap
+        } else {
+          accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
+        }
+        commit('SET_ROUTERS', accessedRouters);
+        resolve();
+      })
+    }
+  }
+};
+
+export default permission;

+ 133 - 0
src/store/modules/user.js

@@ -0,0 +1,133 @@
+import { loginByEmail, logout, getInfo } from 'api/login';
+import { getToken, setToken, removeToken } from 'utils/auth';
+
+const user = {
+  state: {
+    user: '',
+    status: '',
+    code: '',
+    token: getToken(),
+    name: '',
+    avatar: '',
+    introduction: '',
+    roles: [],
+    setting: {
+      articlePlatform: []
+    }
+  },
+
+  mutations: {
+    SET_CODE: (state, code) => {
+      state.code = code;
+    },
+    SET_TOKEN: (state, token) => {
+      state.token = token;
+    },
+    SET_INTRODUCTION: (state, introduction) => {
+      state.introduction = introduction;
+    },
+    SET_SETTING: (state, setting) => {
+      state.setting = setting;
+    },
+    SET_STATUS: (state, status) => {
+      state.status = status;
+    },
+    SET_NAME: (state, name) => {
+      state.name = name;
+    },
+    SET_AVATAR: (state, avatar) => {
+      state.avatar = avatar;
+    },
+    SET_ROLES: (state, roles) => {
+      state.roles = roles;
+    },
+    LOGIN_SUCCESS: () => {
+      console.log('login success')
+    },
+    LOGOUT_USER: state => {
+      state.user = '';
+    }
+  },
+
+  actions: {
+    // 邮箱登录
+    LoginByEmail({ commit }, userInfo) {
+      const email = userInfo.email.trim();
+      return new Promise((resolve, reject) => {
+        loginByEmail(email, userInfo.password).then(response => {
+          const data = response.data;
+          setToken(response.data.token);
+          commit('SET_TOKEN', data.token);
+          resolve();
+        }).catch(error => {
+          reject(error);
+        });
+      });
+    },
+
+    // 获取用户信息
+    GetInfo({ commit, state }) {
+      return new Promise((resolve, reject) => {
+        getInfo(state.token).then(response => {
+          const data = response.data;
+          commit('SET_ROLES', data.role);
+          commit('SET_NAME', data.name);
+          commit('SET_AVATAR', data.avatar);
+          commit('SET_INTRODUCTION', data.introduction);
+          resolve(response);
+        }).catch(error => {
+          reject(error);
+        });
+      });
+    },
+
+    // 第三方验证登录
+    LoginByThirdparty({ commit, state }, code) {
+      return new Promise((resolve, reject) => {
+        commit('SET_CODE', code);
+        loginByThirdparty(state.status, state.email, state.code).then(response => {
+          commit('SET_TOKEN', response.data.token);
+          setToken(response.data.token);
+          resolve();
+        }).catch(error => {
+          reject(error);
+        });
+      });
+    },
+
+    // 登出
+    LogOut({ commit, state }) {
+      return new Promise((resolve, reject) => {
+        logout(state.token).then(() => {
+          commit('SET_TOKEN', '');
+          commit('SET_ROLES', []);
+          removeToken();
+          resolve();
+        }).catch(error => {
+          reject(error);
+        });
+      });
+    },
+
+    // 前端 登出
+    FedLogOut({ commit }) {
+      return new Promise(resolve => {
+        commit('SET_TOKEN', '');
+        removeToken();
+        resolve();
+      });
+    },
+
+    // 动态修改权限
+    ChangeRole({ commit }, role) {
+      return new Promise(resolve => {
+        commit('SET_ROLES', [role]);
+        commit('SET_TOKEN', role);
+        setToken(role);
+        resolve();
+      })
+    }
+  }
+};
+
+export default user;

+ 103 - 0
src/styles/btn.scss

@@ -0,0 +1,103 @@
+$blue:#324157;
+$light-blue:#3A71A8;
+$red:#C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow:#FEC171;
+
+$panGreen: #30B08F;
+
+@mixin colorBtn($color) {
+  background: $color;
+  &:hover {
+    color: $color;
+    &:before, &:after {
+      background: $color;
+    }
+  }
+}
+
+
+.blue-btn {
+  @include colorBtn($blue)
+}
+
+.light-blue-btn{
+  @include colorBtn($light-blue)
+}
+
+
+.red-btn {
+  @include colorBtn($red)
+}
+
+.pink-btn {
+  @include colorBtn($pink)
+}
+
+.green-btn {
+  @include colorBtn($green)
+}
+
+
+.tiffany-btn {
+  @include colorBtn($tiffany)
+}
+
+
+.yellow-btn {
+  @include colorBtn($yellow)
+}
+
+.pan-btn {
+  font-size: 14px;
+  color: #fff;
+  padding: 14px 36px;
+  border-radius: 8px;
+  border: none;
+  outline: none;
+  margin-right: 25px;
+  transition: 600ms ease all;
+  position: relative;
+  display: inline-block;
+  &:hover {
+    background: #fff;
+    &:before, &:after {
+      width: 100%;
+      transition: 600ms ease all;
+    }
+  }
+  &:before, &:after {
+    content: '';
+    position: absolute;
+    top: 0;
+    right: 0;
+    height: 2px;
+    width: 0;
+    transition: 400ms ease all;
+  }
+  &::after {
+    right: inherit;
+    top: inherit;
+    left: 0;
+    bottom: 0;
+  }
+}
+
+.custom-button{
+    display: inline-block;
+    line-height: 1;
+    white-space: nowrap;
+    cursor: pointer;
+    background: #fff;
+    color: #fff;
+    -webkit-appearance: none;
+    text-align: center;
+    box-sizing: border-box;
+    outline: 0;
+    margin: 0;
+    padding: 10px 15px;
+    font-size: 14px;
+    border-radius: 4px;
+}

+ 82 - 0
src/styles/element-ui.scss

@@ -0,0 +1,82 @@
+ //覆盖一些element-ui样式
+ .block-checkbox {
+   display: block;
+ }
+
+ .operation-container {
+   .cell {
+     padding: 10px !important;
+   }
+   .el-button {
+     &:nth-child(3) {
+       margin-top: 10px;
+       margin-left: 0px;
+     }
+     &:nth-child(4) {
+       margin-top: 10px;
+     }
+   }
+ }
+
+ .el-upload {
+   input[type="file"] {
+     display: none !important;
+   }
+ }
+
+ .el-upload__input {
+   display: none;
+ }
+
+ .cell {
+   .el-tag {
+     margin-right: 8px;
+   }
+ }
+
+ .small-padding {
+   .cell {
+     padding-left: 8px;
+     padding-right: 8px;
+   }
+ }
+
+ .status-col {
+   .cell {
+     padding: 0 10px;
+     text-align: center;
+     .el-tag {
+       margin-right: 0px;
+     }
+   }
+ }
+
+ //暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
+ .el-dialog {
+   transform: none;
+   left: 0;
+   position: relative;
+   margin: 0 auto;
+ }
+
+ //文章页textarea修改样式
+ .article-textarea {
+   textarea {
+     padding-right: 40px;
+     resize: none;
+     border: none;
+     border-radius: 0px;
+     border-bottom: 1px solid #bfcbd9;
+   }
+ }
+
+ //element ui upload
+ .upload-container {
+   .el-upload {
+     width: 100%;
+     .el-upload-dragger {
+       width: 100%;
+       height: 200px;
+     }
+   }
+ }

+ 271 - 0
src/styles/index.scss

@@ -0,0 +1,271 @@
+@import './mixin.scss';
+@import './btn.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+
+body {
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+  font-weight: 700;
+}
+
+html {
+  box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+}
+
+.no-padding {
+  padding: 0px !important;
+}
+
+.padding-content {
+  padding: 4px 0;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+.fr {
+  float: right;
+}
+
+.fl {
+  float: left;
+}
+
+.pr-5 {
+  padding-right: 5px;
+}
+
+.pl-5 {
+  padding-left: 5px;
+}
+
+.block {
+  display: block;
+}
+
+.pointer {
+  cursor: pointer;
+}
+
+.inlineBlock {
+  display: block;
+}
+
+code {
+  background: #eef1f6;
+  padding: 15px 10px;
+  margin-bottom: 20px;
+  display: block;
+  line-height: 36px;
+  a {
+    color: #337ab7;
+    cursor: pointer;
+    &:hover {
+      color: rgb(32, 160, 255);
+    }
+  }
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: all .2s ease
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+
+//main-container全局样式
+.app-container {
+  padding: 20px;
+}
+.components-container {
+  margin: 30px 50px;
+  position: relative;
+}
+.pagination-container {
+  margin-top: 30px;
+}
+
+.editor-container .CodeMirror {
+  height: 100%!important;
+}
+
+.text-center{
+  text-align: center
+}
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.sub-navbar {
+  height: 50px;
+  line-height: 50px;
+  position: relative;
+  width: 100%;
+  text-align: right;
+  padding-right: 20px;
+  transition: 600ms ease position;
+  background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+  .subtitle {
+    font-size: 20px;
+    color: #fff;
+  }
+  &.draft {
+    background: #d0d0d0;
+  }
+  &.deleted {
+    background: #d0d0d0;
+  }
+}
+
+.link-type,
+.link-type:focus {
+  color: #337ab7;
+  cursor: pointer;
+  &:hover {
+    color: rgb(32, 160, 255);
+  }
+}
+
+.publishedTag,
+.draftTag,
+.deletedTag {
+  color: #fff;
+  background-color: $panGreen;
+  line-height: 1;
+  text-align: center;
+  margin: 0;
+  padding: 8px 12px;
+  font-size: 14px;
+  border-radius: 4px;
+  position: absolute;
+  left: 20px;
+  top: 10px;
+}
+
+.draftTag {
+  background-color: $yellow;
+}
+
+.deletedTag {
+  background-color: $red;
+}
+
+.input-label {
+  font-size: 14px;
+  color: #48576a;
+  line-height: 1;
+  padding: 11px 5px 11px 0;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: " ";
+    clear: both;
+    height: 0;
+  }
+}
+
+.no-marginLeft {
+  .el-checkbox {
+    margin: 0 20px 15px 0;
+  }
+  .el-checkbox+.el-checkbox {
+    margin-left: 0px;
+  }
+}
+
+.filter-container {
+  padding-bottom: 10px;
+  .filter-item {
+    display: inline-block;
+    vertical-align: middle;
+    margin-bottom: 10px;
+  }
+}
+
+
+//refine vue-multiselect plugin
+.multiselect {
+  line-height: 16px;
+}
+
+.multiselect--active {
+  z-index: 1000 !important;
+}
+
+//refine simplemde
+.simplemde-container{
+  .editor-toolbar.fullscreen,.CodeMirror-fullscreen{
+    z-index: 1003;
+  }
+}
+
+//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+  transform: none;
+  left: 0;
+  position: relative;
+  margin: 0 auto;
+}
+
+//github-corner
+.github-corner:hover .octo-arm {
+  animation: octocat-wave 560ms ease-in-out
+}
+
+@keyframes octocat-wave {
+  0%,
+  100% {
+    transform: rotate(0)
+  }
+  20%,
+  60% {
+    transform: rotate(-25deg)
+  }
+  40%,
+  80% {
+    transform: rotate(10deg)
+  }
+}
+
+@media (max-width:500px) {
+  .github-corner:hover .octo-arm {
+    animation: none
+  }
+  .github-corner .octo-arm {
+    animation: octocat-wave 560ms ease-in-out
+  }
+}

+ 60 - 0
src/styles/mixin.scss

@@ -0,0 +1,60 @@
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}
+
+@mixin scrollBar {
+  &::-webkit-scrollbar-track-piece {
+    background: #d3dce6;
+  }
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #99a9bf;
+    border-radius: 20px;
+  }
+}
+
+@mixin relative {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+@mixin pct($pct) {
+  width: #{$pct};
+  position: relative;
+  margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+  $width: $width/2;
+  $color-border-style: $height solid $color;
+  $transparent-border-style: $width solid transparent;
+  height: 0;
+  width: 0;
+  @if $direction==up {
+    border-bottom: $color-border-style;
+    border-left: $transparent-border-style;
+    border-right: $transparent-border-style;
+  }
+  @else if $direction==right {
+    border-left: $color-border-style;
+    border-top: $transparent-border-style;
+    border-bottom: $transparent-border-style;
+  }
+  @else if $direction==down {
+    border-top: $color-border-style;
+    border-left: $transparent-border-style;
+    border-right: $transparent-border-style;
+  }
+  @else if $direction==left {
+    border-right: $color-border-style;
+    border-top: $transparent-border-style;
+    border-bottom: $transparent-border-style;
+  }
+}

+ 77 - 0
src/styles/sidebar.scss

@@ -0,0 +1,77 @@
+// 侧边栏
+.sidebar-container>.el-menu {
+  width: 100%!important;
+  min-height: 100%;
+}
+
+.sidebar-container .svg-icon {
+  margin-right: 16px;
+}
+
+.hideSidebar .el-submenu>.el-submenu__title,
+.hideSidebar .submenu-title-noDropdown {
+  padding-left: 10px!important;
+}
+
+.hideSidebar .submenu-title-noDropdown span,
+.hideSidebar .el-submenu>.el-submenu__title>span {
+  height: 0;
+  width: 0;
+  overflow: hidden;
+  visibility: hidden;
+  display: inline-block;
+}
+
+.hideSidebar .nest-menu .el-submenu__title {
+  text-align: initial!important;
+  padding-left: 20px!important;
+  span {
+    height: auto;
+    width: auto;
+    visibility: visible;
+    display: inline;
+  }
+  .el-submenu__icon-arrow {
+    display: block!important;
+  }
+}
+
+.hideSidebar .menu-wrapper>.el-menu-item,
+.hideSidebar .submenu-title-noDropdown,
+.hideSidebar .menu-wrapper>.el-submenu .el-submenu__title {
+  text-align: center;
+}
+
+.hideSidebar .el-menu-item .el-submenu__icon-arrow,
+.hideSidebar .el-submenu .el-submenu__title .el-submenu__icon-arrow {
+  display: none;
+}
+
+.hideSidebar .submenu-title-noDropdown {
+  position: relative;
+  span {
+    transition: opacity .3s cubic-bezier(.55, 0, .1, 1);
+    opacity: 0;
+  }
+  &:hover {
+    span {
+      display: block;
+      border-radius: 3px;
+      z-index: 1002;
+      width: 140px;
+      height: 56px;
+      visibility: visible;
+      position: absolute;
+      right: -145px;
+      text-align: left;
+      text-indent: 20px;
+      top: 0px;
+      background-color: #1f2d3d;
+      opacity: 1;
+    }
+  }
+}
+
+.el-submenu .el-menu-item {
+  min-width: 180px!important;
+}

+ 15 - 0
src/utils/auth.js

@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}

+ 8 - 0
src/utils/createUniqueString.js

@@ -0,0 +1,8 @@
+/**
+ * Created by jiachenpan on 17/3/8.
+ */
+export default function createUniqueString() {
+  const timestamp = +new Date() + '';
+  const randomNum = parseInt((1 + Math.random()) * 65536) + '';
+  return (+(randomNum + timestamp)).toString(32);
+}

+ 66 - 0
src/utils/fetch.js

@@ -0,0 +1,66 @@
+import axios from 'axios';
+import { Message } from 'element-ui';
+import store from '../store';
+import { getToken } from 'utils/auth';
+
+// 创建axios实例
+const service = axios.create({
+  baseURL: process.env.BASE_API, // api的base_url
+  timeout: 5000                  // 请求超时时间
+});
+
+// request拦截器
+service.interceptors.request.use(config => {
+  // Do something before request is sent
+  if (store.getters.token) {
+    config.headers['X-Token'] = getToken(); // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
+  }
+  return config;
+}, error => {
+  // Do something with request error
+  console.log(error); // for debug
+  Promise.reject(error);
+})
+
+// respone拦截器
+service.interceptors.response.use(
+  response => response,
+  /**
+  * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
+  * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
+  */
+//  const res = response.data;
+//     if (res.code !== 20000) {
+//       Message({
+//         message: res.message,
+//         type: 'error',
+//         duration: 5 * 1000
+//       });
+//       // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
+//       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
+//         MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
+//           confirmButtonText: '重新登录',
+//           cancelButtonText: '取消',
+//           type: 'warning'
+//         }).then(() => {
+//           store.dispatch('FedLogOut').then(() => {
+//             location.reload();// 为了重新实例化vue-router对象 避免bug
+//           });
+//         })
+//       }
+//       return Promise.reject(error);
+//     } else {
+//       return response.data;
+//     }
+  error => {
+    console.log('err' + error);// for debug
+    Message({
+      message: error.message,
+      type: 'error',
+      duration: 5 * 1000
+    });
+    return Promise.reject(error);
+  }
+)
+
+export default service;

+ 270 - 0
src/utils/index.js

@@ -0,0 +1,270 @@
+/**
+ * Created by jiachenpan on 16/11/18.
+ */
+
+ export function parseTime(time, cFormat) {
+   if (arguments.length === 0) {
+     return null;
+   }
+   const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}';
+   let date;
+   if (typeof time == 'object') {
+     date = time;
+   } else {
+     if (('' + time).length === 10) time = parseInt(time) * 1000;
+     date = new Date(time);
+   }
+   const formatObj = {
+     y: date.getFullYear(),
+     m: date.getMonth() + 1,
+     d: date.getDate(),
+     h: date.getHours(),
+     i: date.getMinutes(),
+     s: date.getSeconds(),
+     a: date.getDay()
+   };
+   const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+     let value = formatObj[key];
+     if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1];
+     if (result.length > 0 && value < 10) {
+       value = '0' + value;
+     }
+     return value || 0;
+   });
+   return time_str;
+ }
+
+ export function formatTime(time, option) {
+   time = +time * 1000;
+   const d = new Date(time);
+   const now = Date.now();
+
+   const diff = (now - d) / 1000;
+
+   if (diff < 30) {
+     return '刚刚'
+   } else if (diff < 3600) { // less 1 hour
+     return Math.ceil(diff / 60) + '分钟前'
+   } else if (diff < 3600 * 24) {
+     return Math.ceil(diff / 3600) + '小时前'
+   } else if (diff < 3600 * 24 * 2) {
+     return '1天前'
+   }
+   if (option) {
+     return parseTime(time, option)
+   } else {
+     return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
+   }
+ }
+
+// 格式化时间
+ export function getQueryObject(url) {
+   url = url == null ? window.location.href : url;
+   const search = url.substring(url.lastIndexOf('?') + 1);
+   const obj = {};
+   const reg = /([^?&=]+)=([^?&=]*)/g;
+   search.replace(reg, (rs, $1, $2) => {
+     const name = decodeURIComponent($1);
+     let val = decodeURIComponent($2);
+     val = String(val);
+     obj[name] = val;
+     return rs;
+   });
+   return obj;
+ }
+
+
+/**
+ *get getByteLen
+ * @param {Sting} val input value
+ * @returns {number} output value
+ */
+ export function getByteLen(val) {
+   let len = 0;
+   for (let i = 0; i < val.length; i++) {
+     if (val[i].match(/[^\x00-\xff]/ig) != null) {
+       len += 1;
+     } else { len += 0.5; }
+   }
+   return Math.floor(len);
+ }
+
+ export function cleanArray(actual) {
+   const newArray = [];
+   for (let i = 0; i < actual.length; i++) {
+     if (actual[i]) {
+       newArray.push(actual[i]);
+     }
+   }
+   return newArray;
+ }
+
+ export function param(json) {
+   if (!json) return '';
+   return cleanArray(Object.keys(json).map(key => {
+     if (json[key] === undefined) return '';
+     return encodeURIComponent(key) + '=' +
+            encodeURIComponent(json[key]);
+   })).join('&');
+ }
+
+ export function param2Obj(url) {
+   const search = url.split('?')[1];
+   if (!search) {
+     return {}
+   }
+   return JSON.parse('{"' + decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}');
+ }
+
+ export function html2Text(val) {
+   const div = document.createElement('div');
+   div.innerHTML = val;
+   return div.textContent || div.innerText;
+ }
+
+ export function objectMerge(target, source) {
+    /* Merges two  objects,
+     giving the last one precedence */
+
+   if (typeof target !== 'object') {
+     target = {};
+   }
+   if (Array.isArray(source)) {
+     return source.slice();
+   }
+   for (const property in source) {
+     if (source.hasOwnProperty(property)) {
+       const sourceProperty = source[property];
+       if (typeof sourceProperty === 'object') {
+         target[property] = objectMerge(target[property], sourceProperty);
+         continue;
+       }
+       target[property] = sourceProperty;
+     }
+   }
+   return target;
+ }
+
+
+ export function scrollTo(element, to, duration) {
+   if (duration <= 0) return;
+   const difference = to - element.scrollTop;
+   const perTick = difference / duration * 10;
+   setTimeout(() => {
+     console.log(new Date())
+     element.scrollTop = element.scrollTop + perTick;
+     if (element.scrollTop === to) return;
+     scrollTo(element, to, duration - 10);
+   }, 10);
+ }
+
+ export function toggleClass(element, className) {
+   if (!element || !className) {
+     return;
+   }
+   let classString = element.className;
+   const nameIndex = classString.indexOf(className);
+   if (nameIndex === -1) {
+     classString += '' + className;
+   } else {
+     classString = classString.substr(0, nameIndex) + classString.substr(nameIndex + className.length);
+   }
+   element.className = classString;
+ }
+
+ export const pickerOptions = [
+   {
+     text: '今天',
+     onClick(picker) {
+       const end = new Date();
+       const start = new Date(new Date().toDateString());
+       end.setTime(start.getTime());
+       picker.$emit('pick', [start, end]);
+     }
+   }, {
+     text: '最近一周',
+     onClick(picker) {
+       const end = new Date(new Date().toDateString());
+       const start = new Date();
+       start.setTime(end.getTime() - 3600 * 1000 * 24 * 7);
+       picker.$emit('pick', [start, end]);
+     }
+   }, {
+     text: '最近一个月',
+     onClick(picker) {
+       const end = new Date(new Date().toDateString());
+       const start = new Date();
+       start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
+       picker.$emit('pick', [start, end]);
+     }
+   }, {
+     text: '最近三个月',
+     onClick(picker) {
+       const end = new Date(new Date().toDateString());
+       const start = new Date();
+       start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
+       picker.$emit('pick', [start, end]);
+     }
+   }]
+
+ export function getTime(type) {
+   if (type === 'start') {
+     return new Date().getTime() - 3600 * 1000 * 24 * 90
+   } else {
+     return new Date(new Date().toDateString())
+   }
+ }
+
+ export function debounce(func, wait, immediate) {
+   let timeout, args, context, timestamp, result;
+
+   const later = function() {
+    // 据上一次触发时间间隔
+     const last = +new Date() - timestamp;
+
+    // 上次被包装函数被调用时间间隔last小于设定时间间隔wait
+     if (last < wait && last > 0) {
+       timeout = setTimeout(later, wait - last);
+     } else {
+       timeout = null;
+      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+       if (!immediate) {
+         result = func.apply(context, args);
+         if (!timeout) context = args = null;
+       }
+     }
+   };
+
+   return function(...args) {
+     context = this;
+     timestamp = +new Date();
+     const callNow = immediate && !timeout;
+    // 如果延时不存在,重新设定延时
+     if (!timeout) timeout = setTimeout(later, wait);
+     if (callNow) {
+       result = func.apply(context, args);
+       context = args = null;
+     }
+
+     return result;
+   };
+ }
+
+
+ export function deepClone(source) {
+   if (!source && typeof source !== 'object') {
+     throw new Error('error arguments', 'shallowClone');
+   }
+   const targetObj = source.constructor === Array ? [] : {};
+   for (const keys in source) {
+     if (source.hasOwnProperty(keys)) {
+       if (source[keys] && typeof source[keys] === 'object') {
+         targetObj[keys] = source[keys].constructor === Array ? [] : {};
+         targetObj[keys] = deepClone(source[keys]);
+       } else {
+         targetObj[keys] = source[keys];
+       }
+     }
+   }
+   return targetObj;
+ }

+ 27 - 0
src/utils/openWindow.js

@@ -0,0 +1,27 @@
+/**
+ *Created by jiachenpan on 16/11/29.
+ * @param {Sting} url
+ * @param {Sting} title
+ * @param {Number} w
+ * @param {Number} h
+ */
+
+export default function openWindow(url, title, w, h) {
+      // Fixes dual-screen position                         Most browsers      Firefox
+  const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
+  const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
+
+  const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
+  const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
+
+  const left = ((width / 2) - (w / 2)) + dualScreenLeft;
+  const top = ((height / 2) - (h / 2)) + dualScreenTop;
+  const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
+
+  // Puts focus on the newWindow
+  if (window.focus) {
+    newWindow.focus();
+  }
+}
+
+

+ 41 - 0
src/utils/validate.js

@@ -0,0 +1,41 @@
+/**
+ * Created by jiachenpan on 16/11/18.
+ */
+
+/* 是否是公司邮箱*/
+export function isWscnEmail(str) {
+  const reg = /^[a-z0-9](?:[-_.+]?[a-z0-9]+)*@wallstreetcn\.com$/i;
+  return reg.test(str.trim());
+}
+
+/* 合法uri*/
+export function validateURL(textval) {
+  const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
+  return urlregex.test(textval);
+}
+
+/* 小写字母*/
+export function validateLowerCase(str) {
+  const reg = /^[a-z]+$/;
+  return reg.test(str);
+}
+
+/* 验证key*/
+// export function validateKey(str) {
+//     var reg = /^[a-z_\-:]+$/;
+//     return reg.test(str);
+// }
+
+/* 大写字母*/
+export function validateUpperCase(str) {
+  const reg = /^[A-Z]+$/;
+  return reg.test(str);
+}
+
+/* 大小写字母*/
+export function validatAlphabets(str) {
+  const reg = /^[A-Za-z]+$/;
+  return reg.test(str);
+}
+
+

+ 179 - 0
src/vendor/Blob.js

@@ -0,0 +1,179 @@
+/* eslint-disable */
+/* Blob.js
+ * A Blob implementation.
+ * 2014-05-27
+ *
+ * By Eli Grey, http://eligrey.com
+ * By Devin Samarin, https://github.com/eboyjr
+ * License: X11/MIT
+ *   See LICENSE.md
+ */
+
+/*global self, unescape */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+ plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
+
+(function (view) {
+    "use strict";
+
+    view.URL = view.URL || view.webkitURL;
+
+    if (view.Blob && view.URL) {
+        try {
+            new Blob;
+            return;
+        } catch (e) {}
+    }
+
+    // Internally we use a BlobBuilder implementation to base Blob off of
+    // in order to support older browsers that only have BlobBuilder
+    var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
+            var
+                get_class = function(object) {
+                    return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
+                }
+                , FakeBlobBuilder = function BlobBuilder() {
+                    this.data = [];
+                }
+                , FakeBlob = function Blob(data, type, encoding) {
+                    this.data = data;
+                    this.size = data.length;
+                    this.type = type;
+                    this.encoding = encoding;
+                }
+                , FBB_proto = FakeBlobBuilder.prototype
+                , FB_proto = FakeBlob.prototype
+                , FileReaderSync = view.FileReaderSync
+                , FileException = function(type) {
+                    this.code = this[this.name = type];
+                }
+                , file_ex_codes = (
+                    "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+                    + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
+                ).split(" ")
+                , file_ex_code = file_ex_codes.length
+                , real_URL = view.URL || view.webkitURL || view
+                , real_create_object_URL = real_URL.createObjectURL
+                , real_revoke_object_URL = real_URL.revokeObjectURL
+                , URL = real_URL
+                , btoa = view.btoa
+                , atob = view.atob
+
+                , ArrayBuffer = view.ArrayBuffer
+                , Uint8Array = view.Uint8Array
+                ;
+            FakeBlob.fake = FB_proto.fake = true;
+            while (file_ex_code--) {
+                FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
+            }
+            if (!real_URL.createObjectURL) {
+                URL = view.URL = {};
+            }
+            URL.createObjectURL = function(blob) {
+                var
+                    type = blob.type
+                    , data_URI_header
+                    ;
+                if (type === null) {
+                    type = "application/octet-stream";
+                }
+                if (blob instanceof FakeBlob) {
+                    data_URI_header = "data:" + type;
+                    if (blob.encoding === "base64") {
+                        return data_URI_header + ";base64," + blob.data;
+                    } else if (blob.encoding === "URI") {
+                        return data_URI_header + "," + decodeURIComponent(blob.data);
+                    } if (btoa) {
+                        return data_URI_header + ";base64," + btoa(blob.data);
+                    } else {
+                        return data_URI_header + "," + encodeURIComponent(blob.data);
+                    }
+                } else if (real_create_object_URL) {
+                    return real_create_object_URL.call(real_URL, blob);
+                }
+            };
+            URL.revokeObjectURL = function(object_URL) {
+                if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
+                    real_revoke_object_URL.call(real_URL, object_URL);
+                }
+            };
+            FBB_proto.append = function(data/*, endings*/) {
+                var bb = this.data;
+                // decode data to a binary string
+                if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
+                    var
+                        str = ""
+                        , buf = new Uint8Array(data)
+                        , i = 0
+                        , buf_len = buf.length
+                        ;
+                    for (; i < buf_len; i++) {
+                        str += String.fromCharCode(buf[i]);
+                    }
+                    bb.push(str);
+                } else if (get_class(data) === "Blob" || get_class(data) === "File") {
+                    if (FileReaderSync) {
+                        var fr = new FileReaderSync;
+                        bb.push(fr.readAsBinaryString(data));
+                    } else {
+                        // async FileReader won't work as BlobBuilder is sync
+                        throw new FileException("NOT_READABLE_ERR");
+                    }
+                } else if (data instanceof FakeBlob) {
+                    if (data.encoding === "base64" && atob) {
+                        bb.push(atob(data.data));
+                    } else if (data.encoding === "URI") {
+                        bb.push(decodeURIComponent(data.data));
+                    } else if (data.encoding === "raw") {
+                        bb.push(data.data);
+                    }
+                } else {
+                    if (typeof data !== "string") {
+                        data += ""; // convert unsupported types to strings
+                    }
+                    // decode UTF-16 to binary string
+                    bb.push(unescape(encodeURIComponent(data)));
+                }
+            };
+            FBB_proto.getBlob = function(type) {
+                if (!arguments.length) {
+                    type = null;
+                }
+                return new FakeBlob(this.data.join(""), type, "raw");
+            };
+            FBB_proto.toString = function() {
+                return "[object BlobBuilder]";
+            };
+            FB_proto.slice = function(start, end, type) {
+                var args = arguments.length;
+                if (args < 3) {
+                    type = null;
+                }
+                return new FakeBlob(
+                    this.data.slice(start, args > 1 ? end : this.data.length)
+                    , type
+                    , this.encoding
+                );
+            };
+            FB_proto.toString = function() {
+                return "[object Blob]";
+            };
+            FB_proto.close = function() {
+                this.size = this.data.length = 0;
+            };
+            return FakeBlobBuilder;
+        }(view));
+
+    view.Blob = function Blob(blobParts, options) {
+        var type = options ? (options.type || "") : "";
+        var builder = new BlobBuilder();
+        if (blobParts) {
+            for (var i = 0, len = blobParts.length; i < len; i++) {
+                builder.append(blobParts[i]);
+            }
+        }
+        return builder.getBlob(type);
+    };
+}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

+ 141 - 0
src/vendor/Export2Excel.js

@@ -0,0 +1,141 @@
+/* eslint-disable */
+require('script-loader!file-saver');
+require('script-loader!vendor/Blob');
+require('script-loader!xlsx/dist/xlsx.core.min');
+function generateArray(table) {
+    var out = [];
+    var rows = table.querySelectorAll('tr');
+    var ranges = [];
+    for (var R = 0; R < rows.length; ++R) {
+        var outRow = [];
+        var row = rows[R];
+        var columns = row.querySelectorAll('td');
+        for (var C = 0; C < columns.length; ++C) {
+            var cell = columns[C];
+            var colspan = cell.getAttribute('colspan');
+            var rowspan = cell.getAttribute('rowspan');
+            var cellValue = cell.innerText;
+            if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
+
+            //Skip ranges
+            ranges.forEach(function (range) {
+                if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
+                    for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
+                }
+            });
+
+            //Handle Row Span
+            if (rowspan || colspan) {
+                rowspan = rowspan || 1;
+                colspan = colspan || 1;
+                ranges.push({s: {r: R, c: outRow.length}, e: {r: R + rowspan - 1, c: outRow.length + colspan - 1}});
+            }
+            ;
+
+            //Handle Value
+            outRow.push(cellValue !== "" ? cellValue : null);
+
+            //Handle Colspan
+            if (colspan) for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
+        }
+        out.push(outRow);
+    }
+    return [out, ranges];
+};
+
+function datenum(v, date1904) {
+    if (date1904) v += 1462;
+    var epoch = Date.parse(v);
+    return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
+}
+
+function sheet_from_array_of_arrays(data, opts) {
+    var ws = {};
+    var range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}};
+    for (var R = 0; R != data.length; ++R) {
+        for (var C = 0; C != data[R].length; ++C) {
+            if (range.s.r > R) range.s.r = R;
+            if (range.s.c > C) range.s.c = C;
+            if (range.e.r < R) range.e.r = R;
+            if (range.e.c < C) range.e.c = C;
+            var cell = {v: data[R][C]};
+            if (cell.v == null) continue;
+            var cell_ref = XLSX.utils.encode_cell({c: C, r: R});
+
+            if (typeof cell.v === 'number') cell.t = 'n';
+            else if (typeof cell.v === 'boolean') cell.t = 'b';
+            else if (cell.v instanceof Date) {
+                cell.t = 'n';
+                cell.z = XLSX.SSF._table[14];
+                cell.v = datenum(cell.v);
+            }
+            else cell.t = 's';
+
+            ws[cell_ref] = cell;
+        }
+    }
+    if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
+    return ws;
+}
+
+function Workbook() {
+    if (!(this instanceof Workbook)) return new Workbook();
+    this.SheetNames = [];
+    this.Sheets = {};
+}
+
+function s2ab(s) {
+    var buf = new ArrayBuffer(s.length);
+    var view = new Uint8Array(buf);
+    for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
+    return buf;
+}
+
+export function export_table_to_excel(id) {
+    var theTable = document.getElementById(id);
+    console.log('a')
+    var oo = generateArray(theTable);
+    var ranges = oo[1];
+
+    /* original data */
+    var data = oo[0];
+    var ws_name = "SheetJS";
+    console.log(data);
+
+    var wb = new Workbook(), ws = sheet_from_array_of_arrays(data);
+
+    /* add ranges to worksheet */
+    // ws['!cols'] = ['apple', 'banan'];
+    ws['!merges'] = ranges;
+
+    /* add worksheet to workbook */
+    wb.SheetNames.push(ws_name);
+    wb.Sheets[ws_name] = ws;
+
+    var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'});
+
+    saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx")
+}
+
+function formatJson(jsonData) {
+    console.log(jsonData)
+}
+export function export_json_to_excel(th, jsonData, defaultTitle) {
+
+    /* original data */
+
+    var data = jsonData;
+    data.unshift(th);
+    var ws_name = "SheetJS";
+
+    var wb = new Workbook(), ws = sheet_from_array_of_arrays(data);
+
+
+    /* add worksheet to workbook */
+    wb.SheetNames.push(ws_name);
+    wb.Sheets[ws_name] = ws;
+
+    var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'});
+    var title = defaultTitle || '列表'
+    saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), title + ".xlsx")
+}

+ 7 - 0
src/views/charts/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div class="components-container" >
+    <code>
+      这里的所有的图表都基于ECharts,实例代码来源<a href='http://gallery.echartsjs.com/explore.html#sort=rank~timeframe=all~author=all' target='_blank'>gallery</a><br/>其实ECharts封装的很好了,用vue封装是很简单的事情,建议大家自己来封装。<a target='_blank' class='lin' href="https://segmentfault.com/a/1190000009762198#articleHeader16">相关教程</a>
+    </code>
+  </div>
+</template>

+ 24 - 0
src/views/charts/keyboard.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="components-container" style='height:100vh'>
+    <div class='chart-container'>
+      <keyboard-chart height='100%' width='100%'></keyboard-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+  import keyboardChart from 'components/Charts/keyboard';
+
+  export default {
+    components: { keyboardChart }
+  };
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: 90%;
+}
+</style>
+

+ 24 - 0
src/views/charts/keyboard2.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="components-container" style='height:100vh'>
+    <div class='chart-container'>
+      <keyboard-chart2 id='apple' height='100%' width='100%'></keyboard-chart2>
+    </div>
+  </div>
+</template>
+
+<script>
+  import keyboardChart2 from 'components/Charts/keyboard2';
+
+  export default {
+    components: { keyboardChart2 }
+  };
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: 90%;
+}
+</style>
+

+ 24 - 0
src/views/charts/line.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="components-container" style='height:100vh'>
+    <div class='chart-container'>
+      <line-marker height='100%' width='100%'></line-marker>
+    </div>
+  </div>
+</template>
+
+<script>
+  import lineMarker from 'components/Charts/lineMarker';
+
+  export default {
+    components: { lineMarker }
+  };
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: 80%;
+}
+</style>
+

+ 25 - 0
src/views/charts/mixChart.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="components-container" style='height:100vh'>
+    <div class='chart-container'>
+      <mix-chart id='apple' height='100%' width='100%'></mix-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+  import mixChart from 'components/Charts/mixChart';
+
+  export default {
+    components: { mixChart }
+  };
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: 90%;
+  padding-bottom: 40px;
+}
+</style>
+

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio