baishaojie 6 giorni fa
commit
0a8c68a1a5
100 ha cambiato i file con 5043 aggiunte e 0 eliminazioni
  1. 14 0
      .editorconfig
  2. 30 0
      .env.development
  3. 18 0
      .env.production
  4. 8 0
      .env.staging
  5. 4 0
      .eslintignore
  6. 198 0
      .eslintrc.js
  7. 16 0
      .gitignore
  8. 5 0
      .travis.yml
  9. 1 0
      2020-02-19.txp
  10. 21 0
      LICENSE
  11. 96 0
      README-zh.md
  12. 89 0
      README.md
  13. 5 0
      babel.config.js
  14. 35 0
      build/index.js
  15. 96 0
      debug.log
  16. 24 0
      jest.config.js
  17. 116 0
      mock/article.js
  18. 69 0
      mock/index.js
  19. 111 0
      mock/menu.js
  20. 68 0
      mock/mock-server.js
  21. 29 0
      mock/table.js
  22. 85 0
      mock/user.js
  23. 76 0
      package.json
  24. 8 0
      postcss.config.js
  25. 0 0
      public/datas/myMap.json
  26. BIN
      public/favicon.ico
  27. 17 0
      public/index.html
  28. 72 0
      src/App.vue
  29. 41 0
      src/api/article.js
  30. 25 0
      src/api/china.js
  31. 480 0
      src/api/common.js
  32. 49 0
      src/api/dept.js
  33. 11 0
      src/api/menu.js
  34. 17 0
      src/api/remote-search.js
  35. 9 0
      src/api/table.js
  36. 25 0
      src/api/user.js
  37. BIN
      src/assets/404_images/404.png
  38. BIN
      src/assets/404_images/404_cloud.png
  39. BIN
      src/assets/cow.jpg
  40. BIN
      src/assets/custom-theme/fonts/element-icons.ttf
  41. 0 0
      src/assets/custom-theme/index.css
  42. BIN
      src/assets/images/1.png
  43. BIN
      src/assets/images/2.png
  44. BIN
      src/assets/images/indexicon1.png
  45. BIN
      src/assets/images/indexicon2.png
  46. BIN
      src/assets/images/indexicon3.png
  47. BIN
      src/assets/images/indexicon4.png
  48. BIN
      src/assets/images/login-bg.jpg
  49. BIN
      src/assets/images/login-bg1.jpg
  50. BIN
      src/assets/images/login-bujian.png
  51. BIN
      src/assets/images/login-l.jpg
  52. BIN
      src/assets/images/login-r.jpg
  53. BIN
      src/assets/images/login.png
  54. BIN
      src/assets/images/logo.png
  55. BIN
      src/assets/images/logo1.png
  56. BIN
      src/assets/images/logo_u3.png
  57. 78 0
      src/components/Breadcrumb/index.vue
  58. 99 0
      src/components/Encapsulation/index.vue
  59. 54 0
      src/components/GithubCorner/index.vue
  60. 44 0
      src/components/Hamburger/index.vue
  61. 180 0
      src/components/HeaderSearch/index.vue
  62. 101 0
      src/components/Pagination/index.vue
  63. 140 0
      src/components/PanThumb/index.vue
  64. 60 0
      src/components/Screenfull/index.vue
  65. 128 0
      src/components/Simple/index.vue
  66. 62 0
      src/components/SvgIcon/index.vue
  67. 113 0
      src/components/TextHoverEffect/Mallki.vue
  68. 111 0
      src/components/Tinymce/components/EditorImage.vue
  69. 59 0
      src/components/Tinymce/dynamicLoadScript.js
  70. 237 0
      src/components/Tinymce/index.vue
  71. 7 0
      src/components/Tinymce/plugins.js
  72. 6 0
      src/components/Tinymce/toolbar.js
  73. 356 0
      src/components/TreeSelect/index.vue
  74. 134 0
      src/components/Upload/SingleImage.vue
  75. 130 0
      src/components/Upload/SingleImage2.vue
  76. 157 0
      src/components/Upload/SingleImage3.vue
  77. 138 0
      src/components/UploadExcel/index.vue
  78. 114 0
      src/data.js
  79. 49 0
      src/directive/clipboard/clipboard.js
  80. 13 0
      src/directive/clipboard/index.js
  81. 77 0
      src/directive/el-drag-dialog/drag.js
  82. 13 0
      src/directive/el-drag-dialog/index.js
  83. 41 0
      src/directive/el-table/adaptive.js
  84. 13 0
      src/directive/el-table/index.js
  85. 30 0
      src/directive/enterToNext/enterToNext.js
  86. 13 0
      src/directive/enterToNext/index.js
  87. 13 0
      src/directive/permission/index.js
  88. 22 0
      src/directive/permission/permission.js
  89. 91 0
      src/directive/sticky.js
  90. 13 0
      src/directive/waves/index.js
  91. 26 0
      src/directive/waves/waves.css
  92. 72 0
      src/directive/waves/waves.js
  93. 68 0
      src/filters/index.js
  94. 9 0
      src/icons/index.js
  95. 0 0
      src/icons/svg/SQL管理.svg
  96. 0 0
      src/icons/svg/dashboard.svg
  97. 1 0
      src/icons/svg/elec.svg
  98. 1 0
      src/icons/svg/example.svg
  99. 1 0
      src/icons/svg/exit-fullscreen.svg
  100. 1 0
      src/icons/svg/eye-open.svg

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 30 - 0
.env.development

@@ -0,0 +1,30 @@
+# just a flag
+ENV = 'development'
+
+# base api
+# VUE_APP_BASE_API = '/dev-api'
+# 线上正式地址
+#VUE_APP_BASE_API = 'http://eam.modernfarming.cn:8000/'
+
+
+
+
+# 线上测试
+VUE_APP_BASE_API = 'http://tmrwatch.cn:8082/'
+
+# 白少后台本地
+   # VUE_APP_BASE_API = 'http://192.168.1.78:8081/'
+
+#VUE_APP_BASE_API = 'http://36.155.144.182:18090/'
+
+
+
+
+# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
+# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
+# It only does one thing by converting all import() to require().
+# This configuration can significantly increase the speed of hot updates,
+# when you have a large number of pages.
+# Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
+
+VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 18 - 0
.env.production

@@ -0,0 +1,18 @@
+# just a flag
+ENV = 'production'
+
+# base api
+#
+VUE_APP_BASE_API = '/'
+# 测试线
+#VUE_APP_BASE_API = 'http://tmrwatch.cn:8082/'
+# 后台本地
+# VUE_APP_BASE_API = 'http://192.168.1.77:8082/'
+# 牧场测试线
+# VUE_APP_BASE_API = 'http://36.155.144.182:18090/'
+# 正式线
+# VUE_APP_BASE_API = 'http://eam.modernfarming.cn:8000/'
+
+# VUE_APP_BASE_API = 'http://127.0.0.1:8082/'
+
+# VUE_APP_BASE_API = 'http://36.155.144.182:18090/'

+ 8 - 0
.env.staging

@@ -0,0 +1,8 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'
+
+# base api
+VUE_APP_BASE_API = '/stage-api'
+

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 198 - 0
.eslintrc.js

@@ -0,0 +1,198 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  // extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    // "vue/max-attributes-per-line": [2, {
+    //   "singleline": 10,
+    //   "multiline": {
+    //     "max": 1,
+    //     "allowFirstLine": false
+    //   }
+    // }],
+    // "vue/singleline-html-element-content-newline": "off",
+    // "vue/multiline-html-element-content-newline":"off",
+    // "vue/name-property-casing": ["error", "PascalCase"],
+    // "vue/no-v-html": "off",
+    // 'accessor-pairs': 2,
+    // 'arrow-spacing': [2, {
+    //   'before': true,
+    //   'after': true
+    // }],
+    // 'block-spacing': [2, 'always'],
+    // 'brace-style': [2, '1tbs', {
+    //   'allowSingleLine': true
+    // }],
+    // 'camelcase': [0, {
+    //   'properties': 'always'
+    // }],
+    // 'comma-dangle': [2, 'never'],
+    // 'comma-spacing': [2, {
+    //   'before': false,
+    //   'after': true
+    // }],
+    // 'comma-style': [2, 'last'],
+    // 'constructor-super': 2,
+    // 'curly': [2, 'multi-line'],
+    // 'dot-location': [2, 'property'],
+    // 'eol-last': 2,
+    // 'eqeqeq': ["error", "always", {"null": "ignore"}],
+    // 'generator-star-spacing': [2, {
+    //   'before': true,
+    //   'after': true
+    // }],
+    // 'handle-callback-err': [2, '^(err|error)$'],
+    // 'indent': [2, 2, {
+    //   'SwitchCase': 1
+    // }],
+    // 'jsx-quotes': [2, 'prefer-single'],
+    // 'key-spacing': [2, {
+    //   'beforeColon': false,
+    //   'afterColon': true
+    // }],
+    // 'keyword-spacing': [2, {
+    //   'before': true,
+    //   'after': true
+    // }],
+    // 'new-cap': [2, {
+    //   'newIsCap': true,
+    //   'capIsNew': false
+    // }],
+    // 'new-parens': 2,
+    // 'no-array-constructor': 2,
+    // 'no-caller': 2,
+    // 'no-console': 'off',
+    // 'no-class-assign': 2,
+    // 'no-cond-assign': 2,
+    // 'no-const-assign': 2,
+    // 'no-control-regex': 0,
+    // 'no-delete-var': 2,
+    // 'no-dupe-args': 2,
+    // 'no-dupe-class-members': 2,
+    // 'no-dupe-keys': 2,
+    // 'no-duplicate-case': 2,
+    // 'no-empty-character-class': 2,
+    // 'no-empty-pattern': 2,
+    // 'no-eval': 2,
+    // 'no-ex-assign': 2,
+    // 'no-extend-native': 2,
+    // 'no-extra-bind': 2,
+    // 'no-extra-boolean-cast': 2,
+    // 'no-extra-parens': [2, 'functions'],
+    // 'no-fallthrough': 2,
+    // 'no-floating-decimal': 2,
+    // 'no-func-assign': 2,
+    // 'no-implied-eval': 2,
+    // 'no-inner-declarations': [2, 'functions'],
+    // 'no-invalid-regexp': 2,
+    // 'no-irregular-whitespace': 2,
+    // 'no-iterator': 2,
+    // 'no-label-var': 2,
+    // 'no-labels': [2, {
+    //   'allowLoop': false,
+    //   'allowSwitch': false
+    // }],
+    // 'no-lone-blocks': 2,
+    // 'no-mixed-spaces-and-tabs': 2,
+    // 'no-multi-spaces': 2,
+    // 'no-multi-str': 2,
+    // 'no-multiple-empty-lines': [2, {
+    //   'max': 1
+    // }],
+    // 'no-native-reassign': 2,
+    // 'no-negated-in-lhs': 2,
+    // 'no-new-object': 2,
+    // 'no-new-require': 2,
+    // 'no-new-symbol': 2,
+    // 'no-new-wrappers': 2,
+    // 'no-obj-calls': 2,
+    // 'no-octal': 2,
+    // 'no-octal-escape': 2,
+    // 'no-path-concat': 2,
+    // 'no-proto': 2,
+    // 'no-redeclare': 2,
+    // 'no-regex-spaces': 2,
+    // 'no-return-assign': [2, 'except-parens'],
+    // 'no-self-assign': 2,
+    // 'no-self-compare': 2,
+    // 'no-sequences': 2,
+    // 'no-shadow-restricted-names': 2,
+    // 'no-spaced-func': 2,
+    // 'no-sparse-arrays': 2,
+    // 'no-this-before-super': 2,
+    // 'no-throw-literal': 2,
+    // 'no-trailing-spaces': 2,
+    // 'no-undef': 2,
+    // 'no-undef-init': 2,
+    // 'no-unexpected-multiline': 2,
+    // 'no-unmodified-loop-condition': 2,
+    // 'no-unneeded-ternary': [2, {
+    //   'defaultAssignment': false
+    // }],
+    // 'no-unreachable': 2,
+    // 'no-unsafe-finally': 2,
+    // 'no-unused-vars': [2, {
+    //   'vars': 'all',
+    //   'args': 'none'
+    // }],
+    // 'no-useless-call': 2,
+    // 'no-useless-computed-key': 2,
+    // 'no-useless-constructor': 2,
+    // 'no-useless-escape': 0,
+    // 'no-whitespace-before-property': 2,
+    // 'no-with': 2,
+    // 'one-var': [2, {
+    //   'initialized': 'never'
+    // }],
+    // 'operator-linebreak': [2, 'after', {
+    //   'overrides': {
+    //     '?': 'before',
+    //     ':': 'before'
+    //   }
+    // }],
+    // 'padded-blocks': [2, 'never'],
+    // 'quotes': [2, 'single', {
+    //   'avoidEscape': true,
+    //   'allowTemplateLiterals': true
+    // }],
+    // 'semi': [2, 'never'],
+    // 'semi-spacing': [2, {
+    //   'before': false,
+    //   'after': true
+    // }],
+    // 'space-before-blocks': [2, 'always'],
+    // 'space-before-function-paren': [2, 'never'],
+    // 'space-in-parens': [2, 'never'],
+    // 'space-infix-ops': 2,
+    // 'space-unary-ops': [2, {
+    //   'words': true,
+    //   'nonwords': false
+    // }],
+    // 'spaced-comment': [2, 'always', {
+    //   'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    // }],
+    // 'template-curly-spacing': [2, 'never'],
+    // 'use-isnan': 2,
+    // 'valid-typeof': 2,
+    // 'wrap-iife': [2, 'any'],
+    // 'yield-star-spacing': [2, 'both'],
+    // 'yoda': [2, 'never'],
+    // 'prefer-const': 2,
+    // 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    // 'object-curly-spacing': [2, 'always', {
+    //   objectsInObjects: false
+    // }],
+    // 'array-bracket-spacing': [2, 'never']
+  }
+}

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+package-lock.json
+tests/**/coverage/
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 5 - 0
.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js: 10
+script: npm run test
+notifications:
+  email: false

File diff suppressed because it is too large
+ 1 - 0
2020-02-19.txp


+ 21 - 0
LICENSE

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

+ 96 - 0
README-zh.md

@@ -0,0 +1,96 @@
+# vue-admin-template
+
+> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
+
+[线上地址](http://panjiachen.github.io/vue-admin-template)
+
+[国内访问](https://panjiachen.gitee.io/vue-admin-template)
+
+目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
+
+## Extra
+
+如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
+
+## 相关项目
+
+[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
+
+[electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+
+[vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
+
+写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
+
+- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
+- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
+- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
+- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
+- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
+
+## Build Setup
+
+```bash
+# 克隆项目
+git clone https://github.com/PanJiaChen/vue-admin-template.git
+
+# 进入项目目录
+cd vue-admin-template
+
+# 安装依赖
+npm install
+
+# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
+npm install --registry=https://registry.npm.taobao.org
+
+# 启动服务
+npm run dev
+```
+
+浏览器访问 [http://localhost:9528](http://localhost:9528)
+
+## 发布
+
+```bash
+# 构建测试环境
+npm run build:stage
+
+# 构建生产环境
+npm run build:prod
+```
+
+## 其它
+
+```bash
+# 预览发布环境效果
+npm run preview
+
+# 预览发布环境效果 + 静态资源分析
+npm run preview -- --report
+
+# 代码格式检查
+npm run lint
+
+# 代码格式检查并自动修复
+npm run lint -- --fix
+```
+
+更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
+
+## Demo
+
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
+
+Copyright (c) 2017-present PanJiaChen

+ 89 - 0
README.md

@@ -0,0 +1,89 @@
+# vue-admin-template
+
+English | [简体中文](./README-zh.md)
+
+> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
+
+**Live demo:** http://panjiachen.github.io/vue-admin-template
+
+
+**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
+
+## Build Setup
+
+
+```bash
+# clone the project
+git clone https://github.com/PanJiaChen/vue-admin-template.git
+
+# enter the project directory
+cd vue-admin-template
+
+# install dependency
+npm install
+
+# develop
+npm run dev
+```
+
+This will automatically open http://localhost:9528
+
+## Build
+
+```bash
+# build for test environment
+npm run build:stage
+
+# build for production environment
+npm run build:prod
+```
+
+## Advanced
+
+```bash
+# preview the release environment effect
+npm run preview
+
+# preview the release environment effect + static resource analysis
+npm run preview -- --report
+
+# code format check
+npm run lint
+
+# code format check and auto fix
+npm run lint -- --fix
+```
+
+Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
+
+## Demo
+
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
+
+## Extra
+
+If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
+
+For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
+
+## Related Project
+
+[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
+
+[electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+
+[vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
+
+Copyright (c) 2017-present PanJiaChen

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/app'
+  ]
+}

+ 35 - 0
build/index.js

@@ -0,0 +1,35 @@
+const { run } = require('runjs')
+const chalk = require('chalk')
+const config = require('../vue.config.js')
+const rawArgv = process.argv.slice(2)
+const args = rawArgv.join(' ')
+
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
+  const report = rawArgv.includes('--report')
+
+  run(`vue-cli-service build ${args}`)
+
+  const port = 9526
+  const publicPath = config.publicPath
+
+  var connect = require('connect')
+  var serveStatic = require('serve-static')
+  const app = connect()
+
+  app.use(
+    publicPath,
+    serveStatic('./dist', {
+      index: ['index.html', '/']
+    })
+  )
+
+  app.listen(port, function () {
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
+    if (report) {
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
+    }
+
+  })
+} else {
+  run(`vue-cli-service build ${args}`)
+}

+ 96 - 0
debug.log

@@ -0,0 +1,96 @@
+[0317/174929.466:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0320/173441.003:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0324/171832.361:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0326/114532.451:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0326/174029.035:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0330/102225.637:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0330/160524.844:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0331/180453.024:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0401/162157.520:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0402/165808.951:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0403/112343.031:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0403/134118.452:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0407/113651.340:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0407/180252.092:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0408/150218.094:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0410/104721.642:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0410/155916.867:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0413/145754.944:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0414/141831.460:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0414/154438.334:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0415/153518.201:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0416/112338.997:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0416/161655.335:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0417/141409.629:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0422/155012.381:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0428/175551.742:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0507/161117.954:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0509/181813.959:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0511/155936.922:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0515/155928.607:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0520/145310.286:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0601/113705.004:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0605/140648.507:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0616/160221.674:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0619/111927.316:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0622/133635.922:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0623/161920.161:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0628/142402.809:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0628/171924.492:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0710/131804.495:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0728/124804.485:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[1020/090953.875:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1020/090953.946:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\093e8902-1fd2-4bdc-845e-4b5d52588e23: 系统找不到指定的路径。 (0x3)
+[1021/091839.962:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1021/142543.791:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\1840b57b-7cd4-4a83-bed5-820f5e4ecc2b: 系统找不到指定的路径。 (0x3)
+[1021/142604.947:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[1022/091921.122:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1022/091921.502:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\a9f59173-6a8e-4210-b37c-c134b841ab3d: 系统找不到指定的路径。 (0x3)
+[1023/091645.966:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1026/092312.935:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1029/092453.136:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1030/091454.041:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1030/091454.110:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\e775589e-36e7-4be5-9bbc-354ed384c452: 系统找不到指定的路径。 (0x3)
+[1102/091706.320:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1102/091706.407:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\69566cfd-c1f2-4378-85ca-ed935a9ec0cc: 系统找不到指定的路径。 (0x3)
+[1102/091706.412:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\a3f68fc6-96e0-4253-b126-c25633f2075f: 系统找不到指定的路径。 (0x3)
+[1102/091706.417:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\19da55f5-f39e-41a9-8c9d-be13a57ab546: 系统找不到指定的路径。 (0x3)
+[1103/091226.524:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1104/092817.751:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1105/090713.917:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1209/133625.286:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1209/174823.206:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\4a740d44-1271-4713-9dce-3e30069ea8fc: 系统找不到指定的路径。 (0x3)
+[1209/174844.281:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[1210/091328.541:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1211/091431.090:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1211/171319.341:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\3929d30a-3ef2-432e-b17a-e2498f55d222: 系统找不到指定的路径。 (0x3)
+[1211/171340.418:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[1214/091819.330:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1214/161716.320:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\f760ded5-05b8-41f5-8da1-5ddf816fafed: 系统找不到指定的路径。 (0x3)
+[1214/161737.393:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[1215/091637.319:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1216/120102.387:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\5e34134c-65cb-4ea5-9b7c-e53eaaf190db: 系统找不到指定的路径。 (0x3)
+[1216/120123.691:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[1218/092856.724:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1221/092254.429:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1222/092728.627:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1223/091643.515:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1224/113004.776:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1230/091730.275:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[1231/092046.906:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[0105/092522.679:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[0120/091251.484:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[0120/101617.616:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\d5b84829-c0f6-4e3a-83e5-ec11b2eaa435: 系统找不到指定的路径。 (0x3)
+[0120/101823.898:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0120/112635.024:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\ae5e847a-05e9-4063-bd28-13fbc8d9e1cc: 系统找不到指定的路径。 (0x3)
+[0120/112841.398:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0120/131923.719:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\6c5b7717-d290-46e4-aad8-a940936836c5: 系统找不到指定的路径。 (0x3)
+[0120/132130.039:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0120/144510.172:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\026c4e70-9879-41eb-8a19-5adc3b8eded6: 系统找不到指定的路径。 (0x3)
+[0120/144716.447:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0120/165204.029:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[0121/091354.385:ERROR:directory_reader_win.cc(43)] FindFirstFile: 系统找不到指定的路径。 (0x3)
+[0121/111959.100:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\da89e54a-1283-4d1a-b6f4-21991f6bfa3a: 系统找不到指定的路径。 (0x3)
+[0121/112020.223:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)
+[0121/133405.452:ERROR:filesystem_win.cc(129)] GetFileAttributes C:\Users\zjh\AppData\Local\Google\Chrome\User Data\Crashpad\attachments\823955f6-adf8-44b7-88f1-9ecd67a7ef06: 系统找不到指定的路径。 (0x3)
+[0121/133426.545:ERROR:http_transport_win.cc(276)] WinHttpSendRequest: 操作成功完成。 (0x0)

+ 24 - 0
jest.config.js

@@ -0,0 +1,24 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
+      'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  snapshotSerializers: ['jest-serializer-vue'],
+  testMatch: [
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
+  ],
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 'collectCoverage': true,
+  'coverageReporters': [
+    'lcov',
+    'text-summary'
+  ],
+  testURL: 'http://localhost/'
+}

+ 116 - 0
mock/article.js

@@ -0,0 +1,116 @@
+import Mock from 'mockjs'
+
+const List = []
+const count = 100
+
+const baseContent = '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
+const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
+
+for (let i = 0; i < count; i++) {
+  List.push(Mock.mock({
+    id: '@increment',
+    timestamp: +Mock.Random.date('T'),
+    author: '@first',
+    reviewer: '@first',
+    title: '@title(5, 10)',
+    content_short: 'mock data',
+    content: baseContent,
+    forecast: '@float(0, 100, 2, 2)',
+    importance: '@integer(1, 3)',
+    'type|1': ['CN', 'US', 'JP', 'EU'],
+    'status|1': ['发布', '草稿', '已删'],
+    display_time: '@datetime',
+    comment_disabled: true,
+    pageviews: '@integer(300, 5000)',
+    image_uri,
+    platforms: ['a-platform']
+  }))
+}
+
+export default [
+  {
+    url: '/article/list',
+    type: 'get',
+    response: config => {
+      const { importance, type, title, page = 1, limit = 20, sort } = config.query
+
+      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 {
+        code: 200,
+        data: {
+          total: mockList.length,
+          items: pageList
+        }
+      }
+    }
+  },
+
+  {
+    url: '/article/detail',
+    type: 'get',
+    response: config => {
+      const { id } = config.query
+      for (const article of List) {
+        if (article.id === +id) {
+          return {
+            code: 200,
+            data: article
+          }
+        }
+      }
+    }
+  },
+
+  {
+    url: '/article/pv',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 200,
+        data: {
+          pvData: [
+            { key: 'PC', pv: 1024 },
+            { key: 'mobile', pv: 1024 },
+            { key: 'ios', pv: 1024 },
+            { key: 'android', pv: 1024 }
+          ]
+        }
+      }
+    }
+  },
+
+  {
+    url: '/article/create',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 200,
+        data: 'success'
+      }
+    }
+  },
+
+  {
+    url: '/article/update',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 200,
+        data: 'success'
+      }
+    }
+  }
+]
+

+ 69 - 0
mock/index.js

@@ -0,0 +1,69 @@
+import Mock from 'mockjs'
+import { param2Obj } from '../src/utils'
+
+import user from './user'
+import table from './table'
+import menuAPI from './menu'
+import article from './article'
+const mocks = [
+  ...user,
+  ...table,
+  ...menuAPI,
+  ...article
+]
+
+mockXHR// for front mock
+// please use it cautiously, it will redefine XMLHttpRequest,
+// which will cause many of your third-party libraries to be invalidated(like progress event).
+export function mockXHR() {
+  // mock patch
+  // https://github.com/nuysoft/Mock/issues/300
+  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+  Mock.XHR.prototype.send = function() {
+    if (this.custom.xhr) {
+      this.custom.xhr.withCredentials = this.withCredentials || false
+
+      if (this.responseType) {
+        this.custom.xhr.responseType = this.responseType
+      }
+    }
+    this.proxy_send(...arguments)
+  }
+
+  function XHR2ExpressReqWrap(respond) {
+    return function(options) {
+      let result = null
+      if (respond instanceof Function) {
+        const { body, type, url } = options
+        // https://expressjs.com/en/4x/api.html#req
+        result = respond({
+          method: type,
+          body: JSON.parse(body),
+          query: param2Obj(url)
+        })
+      } else {
+        result = respond
+      }
+      return Mock.mock(result)
+    }
+  }
+
+  for (const i of mocks) {
+    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
+  }
+}
+
+// for mock server
+const responseFake = (url, type, respond) => {
+  return {
+    url: new RegExp(`/mock${url}`),
+    type: type || 'get',
+    response(req, res) {
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+export default mocks.map(route => {
+  return responseFake(route.url, route.type, route.response)
+})

+ 111 - 0
mock/menu.js

@@ -0,0 +1,111 @@
+const menuData =
+[
+  {
+    'id': 1,
+    'path': '/console',
+    'redirect': 'noredirect',
+    'component': 'Layout',
+    'name': 'console',
+    'title': '系统管理',
+    'icon': 'form',
+    'parentId': -1,
+    'children': [
+      {
+        'children': [],
+        'id': 7,
+        'name': 'user',
+        'component': 'console/user/index',
+        'title': '用户管理',
+        'icon': 'user',
+        'parentId': 1,
+        'path': 'user'
+      },
+      {
+        'children': [],
+        'name': 'menu',
+        'component': 'console/menu/index',
+        'id': 8,
+        'title': '菜单管理',
+        'icon': 'table',
+        'parentId': 1,
+        'path': 'menu'
+      },
+      {
+        'children': [],
+        'name': 'role',
+        'component': 'console/role/index',
+        'id': 9,
+        'title': '角色管理',
+        'icon': 'table',
+        'parentId': 1,
+        'path': 'role'
+      },
+      {
+        'children': [],
+        'name': 'dict',
+        'component': 'console/dict/index',
+        'id': 10,
+        'title': '字典管理',
+        'icon': 'table',
+        'parentId': 1,
+        'path': 'dict'
+      },
+      {
+        'children': [],
+        'name': 'dept',
+        'component': 'console/dept/index',
+        'id': 11,
+        'title': '部门管理',
+        'icon': 'table',
+        'parentId': 1,
+        'path': 'dept'
+      }
+    ]
+  },
+  {
+    'id': 22,
+    'name': 'myiframe',
+    'component': 'Iframe',
+    'redirect': 'noredirect',
+    'title': '第三方系统',
+    'icon': 'link',
+    'path': '/myiframe',
+    'parentId': -1,
+    'children': [
+      {
+        'id': 23,
+        'children': [],
+        'name': 'wechat',
+        'component': 'Iframe',
+        'title': '微信',
+        'icon': 'wechat',
+        'parentId': 22,
+        'path': 'wechatUrl?src=https://pc.weixin.qq.com/&name=微信'
+      },
+      {
+        'id': 24,
+        'children': [],
+        'name': 'qq',
+        'component': 'Iframe',
+        'title': '腾讯QQ',
+        'icon': 'qq',
+        'parentId': 22,
+        'path': 'qqUrl?src=https://im.qq.com/index.shtml&name=腾讯QQ'
+      }
+    ]
+  }
+
+]
+
+export default [
+  {
+    url: '/authdata/rolemenus',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 200,
+        data: menuData
+      }
+    }
+  }
+]

+ 68 - 0
mock/mock-server.js

@@ -0,0 +1,68 @@
+const chokidar = require('chokidar')
+const bodyParser = require('body-parser')
+const chalk = require('chalk')
+const path = require('path')
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function registerRoutes(app) {
+  let mockLastIndex
+  const { default: mocks } = require('./index.js')
+  for (const mock of mocks) {
+    app[mock.type](mock.url, mock.response)
+    mockLastIndex = app._router.stack.length
+  }
+  const mockRoutesLength = Object.keys(mocks).length
+  return {
+    mockRoutesLength: mockRoutesLength,
+    mockStartIndex: mockLastIndex - mockRoutesLength
+  }
+}
+
+function unregisterRoutes() {
+  Object.keys(require.cache).forEach(i => {
+    if (i.includes(mockDir)) {
+      delete require.cache[require.resolve(i)]
+    }
+  })
+}
+
+module.exports = app => {
+  // es6 polyfill
+  require('@babel/register')
+
+  // parse app.body
+  // https://expressjs.com/en/4x/api.html#req.body
+  app.use(bodyParser.json())
+  app.use(bodyParser.urlencoded({
+    extended: true
+  }))
+
+  const mockRoutes = registerRoutes(app)
+  var mockRoutesLength = mockRoutes.mockRoutesLength
+  var mockStartIndex = mockRoutes.mockStartIndex
+
+  // watch files, hot reload mock server
+  chokidar.watch(mockDir, {
+    ignored: /mock-server/,
+    ignoreInitial: true
+  }).on('all', (event, path) => {
+    if (event === 'change' || event === 'add') {
+      try {
+        // remove mock routes stack
+        app._router.stack.splice(mockStartIndex, mockRoutesLength)
+
+        // clear routes cache
+        unregisterRoutes()
+
+        const mockRoutes = registerRoutes(app)
+        mockRoutesLength = mockRoutes.mockRoutesLength
+        mockStartIndex = mockRoutes.mockStartIndex
+
+        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
+      } catch (error) {
+        console.log(chalk.redBright(error))
+      }
+    }
+  })
+}

+ 29 - 0
mock/table.js

@@ -0,0 +1,29 @@
+import Mock from 'mockjs'
+
+const data = Mock.mock({
+  'items|30': [{
+    id: '@id',
+    title: '@sentence(10, 20)',
+    'status|1': ['published', 'draft', 'deleted'],
+    author: 'name',
+    display_time: '@datetime',
+    pageviews: '@integer(300, 5000)'
+  }]
+})
+
+export default [
+  {
+    url: '/table/list',
+    type: 'get',
+    response: config => {
+      const items = data.items
+      return {
+        code: 20000,
+        data: {
+          total: items.length,
+          items: items
+        }
+      }
+    }
+  }
+]

+ 85 - 0
mock/user.js

@@ -0,0 +1,85 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    role: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    role: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+export default [
+  // user login
+  {
+    url: '/auth',
+    type: 'post',
+    response: config => {
+      const { username } = config.body
+      const token = tokens[username]
+
+      // mock error
+      if (!token) {
+        return {
+          code: 60204,
+          message: 'Account and password are incorrect.'
+        }
+      }
+
+      return {
+        code: 200,
+        data: token
+      }
+    }
+  },
+
+  // get user info
+  {
+    url: '/authdata/userinfo\.*',
+    type: 'get',
+    response: config => {
+      let { token } = config.query
+      token = 'admin-token'
+      const info = users[token]
+
+      // mock error
+      if (!info) {
+        return {
+          code: 50008,
+          message: 'Login failed, unable to get user details.'
+        }
+      }
+
+      return {
+        code: 200,
+        data: info
+      }
+    }
+  },
+
+  // user logout
+  {
+    url: '/authdata/logout',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 200,
+        data: 'success'
+      }
+    }
+  }
+]

+ 76 - 0
package.json

@@ -0,0 +1,76 @@
+{
+  "name": "vue-admin-template",
+  "version": "4.2.1",
+  "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
+  "author": "Pan <panfree23@gmail.com>",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build:prod": "vue-cli-service build",
+    "build:stage": "vue-cli-service build --mode staging",
+    "preview": "node build/index.js --preview",
+    "lint": "eslint --ext .js,.vue src",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
+  },
+  "dependencies": {
+    "@jiaminghi/data-view": "^2.10.0",
+    "axios": "0.18.1",
+    "echarts": "^4.2.1",
+    "element-ui": "2.12.0",
+    "file-saver": "2.0.1",
+    "fs-extra": "^9.1.0",
+    "fuse.js": "3.4.4",
+    "jquery": "^3.5.1",
+    "js-cookie": "2.2.0",
+    "js-table2excel": "^1.0.3",
+    "moment": "^2.24.0",
+    "node-sass": "^4.14.1",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "screenfull": "4.2.0",
+    "v-charts": "^1.19.0",
+    "vue": "2.6.10",
+    "vue-count-to": "1.0.13",
+    "vue-router": "3.0.6",
+    "vuex": "3.1.0",
+    "xlsx": "0.14.1"
+  },
+  "devDependencies": {
+    "@babel/core": "7.0.0",
+    "@babel/register": "7.0.0",
+    "@vue/cli-plugin-babel": "3.6.0",
+    "@vue/cli-plugin-eslint": "3.6.0",
+    "@vue/cli-plugin-unit-jest": "^3.8.0",
+    "@vue/cli-service": "3.6.0",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "^9.5.1",
+    "babel-core": "7.0.0-bridge.0",
+    "babel-eslint": "10.0.1",
+    "babel-jest": "23.6.0",
+    "chalk": "2.4.2",
+    "connect": "3.6.6",
+    "eslint": "5.15.3",
+    "eslint-plugin-vue": "5.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "mockjs": "1.0.1-beta3",
+    "runjs": "^4.3.2",
+    "sass-loader": "^8.0.2",
+    "script-ext-html-webpack-plugin": "2.1.3",
+    "script-loader": "0.7.2",
+    "serve-static": "^1.13.2",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.2",
+    "vue-template-compiler": "2.6.10"
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ]
+}

+ 8 - 0
postcss.config.js

@@ -0,0 +1,8 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  'plugins': {
+    // to edit target browsers: use "browserslist" field in package.json
+    'autoprefixer': {}
+  }
+}

File diff suppressed because it is too large
+ 0 - 0
public/datas/myMap.json


BIN
public/favicon.ico


+ 17 - 0
public/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="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 72 - 0
src/App.vue

@@ -0,0 +1,72 @@
+<template>
+  <div id="app">
+    <router-view v-if="isRouterAlive" />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App',
+  provide() {
+    return {
+      reload: this.reload
+    }
+  },
+  data() {
+    return {
+      isRouterAlive: true
+    }
+  },
+  methods: {
+    reload() {
+      this.isRouterAlive = false
+      this.$nextTick(() => {
+        this.isRouterAlive = true
+      })
+    }
+  }
+}
+</script>
+<style lang="scss">
+.table-fixed {
+  /deep/ .el-table__fixed-right {
+    height: 100% !important; //设置高优先,以覆盖内联样式
+  }
+
+  /deep/ .el-table__fixed {
+    height: 100% !important; //设置高优先,以覆盖内联样式
+  }
+}
+.el-tree-node:focus > .el-tree-node__content {
+  background-color: #82848a !important;
+  color: #fff;
+}
+.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
+    background-color: #82848a;
+    color: #fff;
+}
+.filter-container .filter-item{
+  margin-right: 3px;
+}
+.fixed-width .el-button--mini{
+  margin-right: 3px;
+}
+.el-date-editor.el-input{
+  margin-right: 3px;
+}
+.dialog-footer{
+  right:30px;position:absolute;bottom:30px
+}
+.el-autocomplete-suggestion li{
+  padding:0 3px!important;
+}
+.del{
+  color:#f56c6c;
+}
+.primary{
+  color: #409eff;
+}
+.success{
+  color: #67c23a;
+}
+</style>

+ 41 - 0
src/api/article.js

@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+export function fetchList(query) {
+  return request({
+    url: '/article/list',
+    method: 'post',
+    params: query
+  })
+}
+
+export function fetchArticle(id) {
+  return request({
+    url: '/article/detail',
+    method: 'get',
+    params: { id }
+  })
+}
+
+export function fetchPv(pv) {
+  return request({
+    url: '/article/pv',
+    method: 'get',
+    params: { pv }
+  })
+}
+
+export function createArticle(data) {
+  return request({
+    url: '/article/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateArticle(data) {
+  return request({
+    url: '/article/update',
+    method: 'post',
+    data
+  })
+}

File diff suppressed because it is too large
+ 25 - 0
src/api/china.js


+ 480 - 0
src/api/common.js

@@ -0,0 +1,480 @@
+import request from '@/utils/request'
+import parseTime from '@/utils/index.js'
+import { data } from 'jquery'
+
+export function postJson(url, data) {
+    return request({
+        url: process.env.VUE_APP_BASE_API + url,
+        method: 'post',
+        timeout: 600000,
+        data
+    })
+}
+
+export function partslistSGv2(data) {
+    return request({
+        url: '/authdata/partslistSGv2',
+        method: 'post',
+        data
+    })
+}
+
+export function startStopRecord(data) {
+    return request({
+        url: '/authdata/startStopRecord',
+        method: 'post',
+        data
+    })
+}
+
+export function GetDataByName(data) {
+    return request({
+        url: '/authdata/GetDataByName',
+        method: 'post',
+        data
+    })
+}
+
+export function GetMaintain(data) {
+    return request({
+        url: '/authdata/maintain',
+        method: 'post',
+        data
+    })
+}
+export function GetReportform(data) {
+    return request({
+        url: '/authdata/GetReportform',
+        method: 'post',
+        data
+    })
+}
+export function requestbyname(data, requestname) {
+    return request({
+        url: '/authdata/' + requestname,
+        method: 'post',
+        data
+    })
+}
+
+export function GetDataByNameXlsx(data) {
+    return request({
+        url: '/authdata/GetDataByName',
+        method: 'post',
+        data,
+        responseType: 'blob'
+    })
+}
+
+export function GetDataByNames(data) {
+    return request({
+        url: '/authdata/GetDataByNames',
+        method: 'post',
+        data
+    })
+}
+
+export function EasSync(data) {
+    return request({
+        url: '/authdata/EasSync',
+        method: 'post',
+        data
+    })
+}
+
+export function SapTrans(data) {
+    return request({
+        url: '/authdata/sap/transfer/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SapQuit(data) {
+    return request({
+        url: '/authdata/sap/quitproof/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SapRef(data) {
+    return request({
+        url: '/authdata/sap/refundproof/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SapUse(data) {
+    return request({
+        url: '/authdata/sap/useproof/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SapLaid(data) {
+    return request({
+        url: '/authdata/sap/laidproof/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SapOrder(data) {
+    return request({
+        url: '/authdata/sap/order/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SrmOrder(data) {
+    return request({
+        url: '/authdata/srm/order/send',
+        method: 'post',
+        data
+    })
+}
+
+export function SapOrder5(data) {
+    return request({
+        url: '/editBigRefunddetailSapStatus',
+        method: 'post',
+        data
+    })
+}
+
+export function SrmOrder5(data) {
+    return request({
+        url: '/editBigRefunddetailSrmStatus',
+        method: 'post',
+        data
+    })
+}
+
+export function PostDataByName(data) {
+    return request({
+        url: '/authdata/PostDataByName',
+        method: 'post',
+        data
+    })
+}
+export function GetUpkeepPlan(data) {
+    return request({
+        url: '/authdata/GetUpkeepPlan',
+        method: 'post',
+        data
+    })
+}
+export function GetAccount(data) {
+    return request({
+        url: '/authdata/GetAccount',
+        method: 'post',
+        timeout: 6000000,
+        data
+    })
+}
+
+export function PostDataByNames(data) {
+    return request({
+        url: '/authdata/PostDataByNames',
+        method: 'post',
+        data
+    })
+}
+
+export function ExecDataByConfig(data) {
+    return request({
+        url: '/authdata/ExecDataByConfig',
+        method: 'post',
+        data
+    })
+}
+
+export function removeimage(data) {
+    return request({
+        url: '/authdata/removeimage',
+        method: 'post',
+        data
+    })
+}
+export function getJson(url, data) {
+    return request({
+        url: url + data,
+        method: 'get'
+    })
+}
+export function getRecuData(data) {
+    return request({
+        url: '/authdata/GetRecuDataByName',
+        method: 'post',
+        data
+    })
+}
+
+export function downloadmailreport(data) {
+    return request({
+        url: '/authdata/downloadmailreport',
+        method: 'post',
+        data
+    })
+}
+
+export function failproccess(data, notify, num) {
+    console.log(data, 'failproccess-闪闪')
+    if (data.data.includes('Duplicate')) {
+        notify({
+            title: '失败',
+            message: '不可以录入重复数据',
+            type: 'error',
+            duration: 2000
+        })
+    } else {
+        notify({
+            title: '失败',
+            message: num && num == 1 ? data.data : '数据存在错误,请校验好重新录入,不可以录入数据', // '数据存在错误,请校验好重新录入,不可以录入数据',
+            type: 'error',
+            duration: 2000
+        })
+    }
+}
+
+export function UpdateDataRelation(data) {
+    return request({
+        url: '/authdata/UpdateDataRelation',
+        method: 'post',
+        data
+    })
+}
+export function Equipment(data) {
+    return request({
+        url: '/data/Equipment',
+        method: 'post',
+        timeout: 6000000,
+        data
+    })
+}
+export function StandardPart(data) {
+    return request({
+        url: '/data/StandardPart',
+        method: 'post',
+        data
+    })
+}
+export function PastureDepartment(data) {
+    return request({
+        url: '/data/PastureDepartment',
+        method: 'post',
+        data
+    })
+}
+export function updateAndInster(data) {
+    return request({
+        url: '/data/updateAndInster',
+        method: 'post',
+        data
+    })
+}
+
+export function transData(a, idStr, pidStr, chindrenStr) {
+    var r = [];
+    var hash = {};
+    var id = idStr;
+    var pid = pidStr;
+    var children = chindrenStr;
+    var i = 0;
+    var j = 0;
+    var len = a.length
+    for (; i < len; i++) {
+        hash[a[i][id]] = a[i]
+    }
+    for (; j < len; j++) {
+        var aVal = a[j];
+        var hashVP = hash[aVal[pid]]
+        if (hashVP) {
+            !hashVP[children] && (hashVP[children] = [])
+            hashVP[children].push(aVal)
+        } else {
+            r.push(aVal)
+        }
+    }
+    return r
+}
+
+export function checkButtons(buttonsList, PermissionButtons) {
+    // console.log(PermissionButtons)
+    for (let i = 0; i < buttonsList.length; i++) {
+        if (buttonsList[i].path === PermissionButtons && buttonsList[i].menu_id && buttonsList[i].path) { // path不为空且menu_id不为空时返回true
+            return true
+        }
+    }
+    return false
+}
+
+export function formatJson(filterVal, jsonData) {
+    return jsonData.map(v =>
+        filterVal.map(j => {
+            if (j === 'timestamp') {
+                return parseTime(v[j])
+            } else {
+                return v[j]
+            }
+        })
+    )
+}
+
+export function DownloadExcel(data, filename) {
+    const content = data
+    const blob = new Blob([content])
+    const fileName = filename + '.xlsx'
+    if ('download' in document.createElement('a')) { // 非IE下载
+        const elink = document.createElement('a')
+        elink.download = fileName
+        elink.style.display = 'none'
+        elink.href = URL.createObjectURL(blob)
+        document.body.appendChild(elink)
+        elink.click()
+        URL.revokeObjectURL(elink.href) // 释放URL 对象
+        document.body.removeChild(elink)
+    } else { // IE10+下载
+        navigator.msSaveBlob(blob, fileName)
+    }
+}
+// 用于获取本月或指定月份的最后一天
+export function getMonthFinalDay(year, month) {
+    var day = ''
+    if (year == null || year == undefined || year == '') {
+        year = new Date().getFullYear()
+    }
+    if (month == null || month == undefined || month == '') {
+        month = new Date().getMonth() + 1
+    }
+    day = new Date(new Date(year, month).setDate(0)).getDate()
+    return year + '-' + month + '-' + day
+}
+
+// 获取工作类别数据
+export function getWorkList() {
+    return request({
+        url: '/authdata/work/class',
+        method: 'get'
+    })
+}
+
+// 获取订单号库
+export function getNumList(data) {
+    return request({
+        url: '/authdata/ensiling',
+        method: 'get',
+        params: data
+    })
+}
+
+// 车辆运转率
+export function getVehicRate(data) {
+    return request({
+        url: '/authdata/running/rate/detail',
+        method: 'get',
+        params: data
+    })
+}
+
+
+export function getVehicRateTotal(data) {
+    return request({
+        url: '/authdata/running/rate',
+        method: 'get',
+        params: data
+    })
+}
+
+// 点检率
+export function getSpotRate(data) {
+    return request({
+        url: '/authdata/spotcheck/report',
+        method: 'get',
+        params: data
+    })
+}
+
+export function saveImgIds(data) {
+    return request({
+        url: '/authdata/PostDataByName',
+        method: 'post',
+        data
+    })
+}
+
+// 主牧场修改
+export function editMainPasture(data) {
+    return request({
+        url: '/authdata/mainpasture/edit',
+        method: 'post',
+        data
+    })
+}
+
+// 查询主牧场
+export function MaintenanceStats(data) {
+    return request({
+        url: '/authdata/maintenance/stats',
+        method: 'post',
+        data
+    })
+}
+
+export function getMainPastur(data) {
+    return request({
+        url: '/authdata/mainpasture/list',
+        method: 'get',
+        params: data
+    })
+}
+
+
+// 查询主牧场
+export function getPartsListSGList(data) {
+    return request({
+        url: '/authdata/sg/parts/list',
+        method: 'post',
+        data
+    })
+}
+
+// 导出验收单
+export function getDownList(data) {
+    return request({
+        url: '/authdata/GetDataByName',
+        method: 'post',
+        data
+    })
+}
+
+// 大数据
+export function getBigData(data) {
+    return request({
+        url: '/authdata/eq/data',
+        method: 'get',
+        params: data
+    })
+}
+
+// 大数据记录
+export function getBigDataRecode(data) {
+    return request({
+        url: '/authdata/eq/data/record',
+        method: 'get',
+        params: data
+    })
+}
+
+// 大数据导出
+export function getDownLoadData(data) {
+    return request({
+        url: '/authdata/eq/data/excel',
+        method: 'get',
+        params: data
+    })
+}

+ 49 - 0
src/api/dept.js

@@ -0,0 +1,49 @@
+import request from '@/utils/request'
+
+export function fetchList(data) {
+  return request({
+    url: '/authdata/GetRecuDataByName',
+    method: 'post',
+    data
+  })
+}
+
+export function fetchArticle(id) {
+  return request({
+    url: '/article/detail',
+    method: 'get',
+    params: { id }
+  })
+}
+
+export function fetchPv(pv) {
+  return request({
+    url: '/article/pv',
+    method: 'get',
+    params: { pv }
+  })
+}
+
+export function createDept(data) {
+  return request({
+    url: '/authdata/PostDataByName',
+    method: 'post',
+    data
+  })
+}
+
+export function updateDept(data) {
+  return request({
+    url: '/article/update',
+    method: 'post',
+    data
+  })
+}
+
+export function getRecuDept(data) {
+  return request({
+    url: '/authdata/getrecudatabyname',
+    method: 'post',
+    data
+  })
+}

+ 11 - 0
src/api/menu.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+/**
+  *根据角色获得可见菜单
+  */
+export function getMenuByRole(data) {
+  return request({
+    url: '/authdata/rolemenus',
+    method: 'post',
+    data
+  })
+}

+ 17 - 0
src/api/remote-search.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+export function searchUser(name) {
+  return request({
+    url: '/search/user',
+    method: 'get',
+    params: { name }
+  })
+}
+
+export function transactionList(query) {
+  return request({
+    url: '/transaction/list',
+    method: 'get',
+    params: query
+  })
+}

+ 9 - 0
src/api/table.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+export function get_table_data(params) {
+  return request({
+    url: '/table/list',
+    method: 'get',
+    params
+  })
+}

+ 25 - 0
src/api/user.js

@@ -0,0 +1,25 @@
+import request from '@/utils/request'
+
+export function login(data) {
+  return request({
+    url: '/auth',
+    method: 'post',
+    data
+  })
+}
+
+export function getInfo() { // token
+  return request({
+    url: '/authdata/userinfo',
+    method: 'get'
+    // ,
+    // params: { token }
+  })
+}
+
+export function logout() {
+  return request({
+    url: '/api/v1/logout',
+    method: 'get'
+  })
+}

BIN
src/assets/404_images/404.png


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


BIN
src/assets/cow.jpg


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


File diff suppressed because it is too large
+ 0 - 0
src/assets/custom-theme/index.css


BIN
src/assets/images/1.png


BIN
src/assets/images/2.png


BIN
src/assets/images/indexicon1.png


BIN
src/assets/images/indexicon2.png


BIN
src/assets/images/indexicon3.png


BIN
src/assets/images/indexicon4.png


BIN
src/assets/images/login-bg.jpg


BIN
src/assets/images/login-bg1.jpg


BIN
src/assets/images/login-bujian.png


BIN
src/assets/images/login-l.jpg


BIN
src/assets/images/login-r.jpg


BIN
src/assets/images/login.png


BIN
src/assets/images/logo.png


BIN
src/assets/images/logo1.png


BIN
src/assets/images/logo_u3.png


+ 78 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 99 - 0
src/components/Encapsulation/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div id="app">
+    <div class="content">
+      <div class="data-container" style="width:98%;margin:0 auto;">
+        <h3>各牧场设备统计表:</h3>
+        <el-table ref="interfaceTable" :data="dataList" border :row-style="rowStyle" :cell-style="cellStyle" style="width: 100%" :span-method="colspanMethod" :show-header="showHeader">
+          <el-table-column label="一级分类" prop="pasture" align="center" width="160" />
+          <el-table-column label="二级分类" prop="pasture1" align="center" />
+          <el-table-column label="三级分类" prop="pasture2" align="center" width="160" />
+          <el-table-column label="类型" prop="type" align="center" />
+          <el-table-column label="宝鸡1" prop="pastureName1" align="center" />
+          <el-table-column label="宝鸡2" prop="pastureName2" align="center" width="160" />
+          <!-- <el-table-column label="物流公司" prop="companyName" align="center" />
+          <el-table-column label="物流单号" prop="logisticsNo" align="center" width="160" />
+          <el-table-column label="发货时间" prop="deliveryTime" align="center" width="140" />
+          <el-table-column label="包裹信息" align="center" prop="arr">
+            <template slot-scope="scope">
+              <div v-if="scope.row.packageLong">
+                <div v-for="(item, index) in scope.row.arr" :key="index">{{ item.msg }}</div>
+              </div>
+              <div v-else>/</div>
+            </template>
+          </el-table-column>
+          <el-table-column label="包材编号" prop="materialsCode" align="center" /> -->
+        </el-table>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import datas from '../../data.js'
+import { handleTableSpan, handleObjectSpanMethod } from '../../util.js'
+export default {
+  components: {},
+  mixins: [],
+  data() {
+    return {
+      border: false,
+      showHeader: false,
+      dataList: [],
+      // 要合并的单元格的rowspan 数据
+      rowspanObj: {},
+      // 要纵向合并的单元格的key数组
+      mergekeys: ['pasture', 'pasture1', 'pasture2', 'logisticsNo', 'arr', 'materialsCode'],
+      rowStyle: { maxHeight: 50 + 'px', height: 45 + 'px' },
+      cellStyle: { padding: 0 + 'px' }
+    }
+  },
+  computed: {},
+  watch: {
+    dataList: function() {
+      this.$nextTick(function() {
+        this.$refs.interfaceTable.$el.background = 'red !important'
+        console.log(this.$refs.interfaceTable.$el)
+        console.log(this.$refs.interfaceTable)
+      })
+    }
+  },
+  created() {},
+  mounted() {
+    this.getDataList()
+  },
+  methods: {
+    getDataList() {
+      this.dataList = datas
+      console.log()
+
+      // 先处理一下数据,拿到要合并单元格的 rowspan 数据
+      this.rowspanObj = handleTableSpan(this.mergekeys, this.dataList)
+      console.log(this.rowspanObj)
+      // const length = 'mm'
+      // const weight = 'g'
+      // const arr = []
+
+      // datas.forEach((element, index) => {
+      //   element.arr = [
+      //     { msg: '长:' + element.packageLong + `${length}` },
+      //     { msg: '宽:' + element.packageWidth + `${length}` },
+      //     { msg: '高:' + element.packageHeight + `${length}` },
+      //     { msg: '重量:' + element.packageWeight + `${weight}` }
+      //   ]
+      //   element.expressPackCode = element.expressPackCode ? element.expressPackCode : '/'
+      //   element.companyName = element.companyName ? element.companyName : '/'
+      //   element.logisticsNo = element.logisticsNo ? element.logisticsNo : '/'
+      //   element.deliveryTime = element.deliveryTime ? element.deliveryTime : '/'
+      //   element.logisticsNo = element.logisticsNo ? element.logisticsNo : '/'
+      //   element.materialsCode = element.materialsCode ? element.materialsCode : '/'
+      // })
+    },
+    colspanMethod(tableObj) {
+      return handleObjectSpanMethod(tableObj, this.mergekeys, this.rowspanObj, true)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

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

@@ -0,0 +1,54 @@
+<template>
+  <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
+    <svg
+      width="80"
+      height="80"
+      viewBox="0 0 250 250"
+      style="fill:#40c9c6; color:#fff;"
+      aria-hidden="true"
+    >
+      <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
+      <path
+        d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
+        fill="currentColor"
+        style="transform-origin: 130px 106px;"
+        class="octo-arm"
+      />
+      <path
+        d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
+        fill="currentColor"
+        class="octo-body"
+      />
+    </svg>
+  </a>
+</template>
+
+<style scoped>
+.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
+  }
+}
+</style>

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

@@ -0,0 +1,44 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 180 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div :class="{'show':show}" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select
+      ref="headerSearchSelect"
+      v-model="search"
+      :remote-method="querySearch"
+      filterable
+      default-first-option
+      remote
+      placeholder="输入设备编号或名称"
+      class="header-search-select"
+      @change="change"
+    >
+      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script>
+// fuse is a lightweight fuzzy-search module
+// make search results more in line with expectations
+import Fuse from 'fuse.js'
+import path from 'path'
+
+export default {
+  name: 'HeaderSearch',
+  data() {
+    return {
+      search: '',
+      options: [],
+      searchPool: [],
+      show: false,
+      fuse: undefined
+    }
+  },
+  computed: {
+    routes() {
+      return this.$store.getters.permission_routes
+    }
+  },
+  watch: {
+    routes() {
+      this.searchPool = this.generateRoutes(this.routes)
+    },
+    searchPool(list) {
+      this.initFuse(list)
+    },
+    show(value) {
+      if (value) {
+        document.body.addEventListener('click', this.close)
+      } else {
+        document.body.removeEventListener('click', this.close)
+      }
+    }
+  },
+  mounted() {
+    this.searchPool = this.generateRoutes(this.routes)
+  },
+  methods: {
+    click() {
+      this.show = !this.show
+      if (this.show) {
+        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
+      }
+    },
+    close() {
+      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
+      this.options = []
+      this.show = false
+    },
+    change(val) {
+      this.$router.push(val.path)
+      this.search = ''
+      this.options = []
+      this.$nextTick(() => {
+        this.show = false
+      })
+    },
+    initFuse(list) {
+      this.fuse = new Fuse(list, {
+        shouldSort: true,
+        threshold: 0.4,
+        location: 0,
+        distance: 100,
+        maxPatternLength: 32,
+        minMatchCharLength: 1,
+        keys: [{
+          name: 'title',
+          weight: 0.7
+        }, {
+          name: 'path',
+          weight: 0.3
+        }]
+      })
+    },
+    // Filter out the routes that can be displayed in the sidebar
+    // And generate the internationalized title
+    generateRoutes(routes, basePath = '/', prefixTitle = []) {
+      let res = []
+
+      for (const router of routes) {
+        // skip hidden router
+        if (router.hidden) { continue }
+
+        const data = {
+          path: path.resolve(basePath, router.path),
+          title: [...prefixTitle]
+        }
+
+        if (router.meta && router.meta.title) {
+          data.title = [...data.title, router.meta.title]
+
+          if (router.redirect !== 'noRedirect') {
+            // only push the routes with title
+            // special case: need to exclude parent router without redirect
+            res.push(data)
+          }
+        }
+
+        // recursive child routes
+        if (router.children) {
+          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
+          if (tempRoutes.length >= 1) {
+            res = [...res, ...tempRoutes]
+          }
+        }
+      }
+      return res
+    },
+    querySearch(query) {
+      if (query !== '') {
+        this.options = this.fuse.search(query)
+      } else {
+        this.options = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+
+    /deep/ .el-input__inner {
+      border-radius: 0;
+      border: 0;
+      padding-left: 0;
+      padding-right: 0;
+      box-shadow: none !important;
+      border-bottom: 1px solid #d9d9d9;
+      vertical-align: middle;
+    }
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>

+ 101 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scroll-to'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 10
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50, 100]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 16px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

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

@@ -0,0 +1,140 @@
+<template>
+  <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
+    <div class="pan-info">
+      <div class="pan-info-roles-container">
+        <slot />
+      </div>
+    </div>
+    <img :src="image" class="pan-thumb">
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PanThumb',
+  props: {
+    image: {
+      type: String,
+      required: true
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    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>

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

@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+  </div>
+</template>
+
+<script>
+import screenfull from 'screenfull'
+
+export default {
+  name: 'Screenfull',
+  data() {
+    return {
+      isFullscreen: false
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  beforeDestroy() {
+    this.destroy()
+  },
+  methods: {
+    click() {
+      if (!screenfull.enabled) {
+        this.$message({
+          message: 'you browser can not work',
+          type: 'warning'
+        })
+        return false
+      }
+      screenfull.toggle()
+    },
+    change() {
+      this.isFullscreen = screenfull.isFullscreen
+    },
+    init() {
+      if (screenfull.enabled) {
+        screenfull.on('change', this.change)
+      }
+    },
+    destroy() {
+      if (screenfull.enabled) {
+        screenfull.off('change', this.change)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.screenfull-svg {
+  display: inline-block;
+  cursor: pointer;
+  fill: #5a5e66;;
+  width: 20px;
+  height: 20px;
+  vertical-align: 10px;
+}
+</style>

+ 128 - 0
src/components/Simple/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="">
+    <el-table
+      :data="listData"
+      :span-method="objectSpanMethod"
+      class="tableArea"
+      style="width: 100%"
+    >
+      <!-- <el-table :data="listData" class="tableArea" style="width: 100%"> -->
+      <el-table-column prop="type" label="序号" align="center" width="200" />
+      <el-table-column prop="sheetType" label="" />
+      <el-table-column prop="taskKey" label="宝鸡1" />
+      <el-table-column prop="templateUrl" label="宝鸡2" />
+      <el-table-column label="操作">
+        <template slot-scope="scope">
+          <el-tooltip class="item" effect="dark" content="修改" placement="top-start">
+            <i class="el-icon-edit-outline" @click="modify(scope)" />
+          </el-tooltip>
+          <el-tooltip class="item" effect="dark" content="删除" placement="top-start">
+            <i class="el-icon-delete" @click="deleteData(scope)" />
+          </el-tooltip>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+<script>
+
+import { handleTableSpan, handleObjectSpanMethod } from '../../util.js'
+
+export default {
+  name: 'MyNeedDeal',
+  data() {
+    return {
+      listData: [],
+      spanObj: {},
+      mergekeys: ['type', 'sheetType', 'templateUrl']
+    }
+  },
+  created() {
+  },
+  mounted() {
+    this.queryData()
+  },
+  methods: {
+    queryData() { // 查询操作
+      this.listData = [
+        {
+          id: 1,
+          type: 1,
+          sheetType: '事件单',
+          taskKey: 'shijian_01',
+          templateUrl: '/shijian_01'
+        },
+        {
+          id: 2,
+          type: 1,
+          sheetType: '事件单',
+          taskKey: 'shijian_02',
+          templateUrl: '/shijian_02'
+        },
+        {
+          id: 3,
+          type: 1,
+          sheetType: '事件单',
+          taskKey: 'shijian_02',
+          templateUrl: '/shijian_03'
+        },
+        {
+          id: 4,
+          type: 2,
+          sheetType: '问题单',
+          taskKey: 'wenti_01',
+          templateUrl: '/wenti_01'
+        },
+        {
+          id: 5,
+          type: 2,
+          sheetType: '问题单',
+          taskKey: 'wenti_02',
+          templateUrl: '/wenti_02'
+        },
+        {
+          id: 6,
+          type: 2,
+          sheetType: '问题单',
+          taskKey: 'wenti_03',
+          templateUrl: '/wenti_03'
+        }
+      ]
+      this.handleSpan()
+    },
+    handleSpan() {
+      this.mergekeys.forEach(key => {
+        this.spanObj[key] = []
+        let position = 0
+        this.listData.forEach((item, index) => {
+          if (index === 0) {
+            this.spanObj[key].push(1)
+            position = 0
+          } else {
+            if (this.listData[index][key] === this.listData[index - 1][key]) {
+              this.spanObj[key][position] += 1
+              this.spanObj[key].push(0)
+            } else {
+              this.spanObj[key].push(1)
+              position = index
+            }
+          }
+        })
+      })
+    },
+    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
+      for (let i = 0; i < this.mergekeys.length; i++) {
+        if (column.property === this.mergekeys[i]) {
+          const _row = this.spanObj[this.mergekeys[i]][rowIndex]
+          const _col = _row > 0 ? 1 : 0
+          return {
+            rowspan: _row,
+            colspan: _col
+          }
+        }
+      }
+    }
+  }
+}
+</script>
+

+ 62 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 113 - 0
src/components/TextHoverEffect/Mallki.vue

@@ -0,0 +1,113 @@
+<template>
+  <a :class="className" class="link--mallki" href="#">
+    {{ text }}
+    <span :data-letters="text" />
+    <span :data-letters="text" />
+  </a>
+</template>
+
+<script>
+export default {
+  props: {
+    className: {
+      type: String,
+      default: ''
+    },
+    text: {
+      type: String,
+      default: 'vue-element-admin'
+    }
+  }
+}
+</script>
+
+<style>
+/* Mallki */
+
+.link--mallki {
+  font-weight: 800;
+  color: #4dd9d5;
+  font-family: 'Dosis', sans-serif;
+  -webkit-transition: color 0.5s 0.25s;
+  transition: color 0.5s 0.25s;
+  overflow: hidden;
+  position: relative;
+  display: inline-block;
+  line-height: 1;
+  outline: none;
+  text-decoration: none;
+}
+
+.link--mallki:hover {
+  -webkit-transition: none;
+  transition: none;
+  color: transparent;
+}
+
+.link--mallki::before {
+  content: '';
+  width: 100%;
+  height: 6px;
+  margin: -3px 0 0 0;
+  background: #3888fa;
+  position: absolute;
+  left: 0;
+  top: 50%;
+  -webkit-transform: translate3d(-100%, 0, 0);
+  transform: translate3d(-100%, 0, 0);
+  -webkit-transition: -webkit-transform 0.4s;
+  transition: transform 0.4s;
+  -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+}
+
+.link--mallki:hover::before {
+  -webkit-transform: translate3d(100%, 0, 0);
+  transform: translate3d(100%, 0, 0);
+}
+
+.link--mallki span {
+  position: absolute;
+  height: 50%;
+  width: 100%;
+  left: 0;
+  top: 0;
+  overflow: hidden;
+}
+
+.link--mallki span::before {
+  content: attr(data-letters);
+  color: red;
+  position: absolute;
+  left: 0;
+  width: 100%;
+  color: #3888fa;
+  -webkit-transition: -webkit-transform 0.5s;
+  transition: transform 0.5s;
+}
+
+.link--mallki span:nth-child(2) {
+  top: 50%;
+}
+
+.link--mallki span:first-child::before {
+  top: 0;
+  -webkit-transform: translate3d(0, 100%, 0);
+  transform: translate3d(0, 100%, 0);
+}
+
+.link--mallki span:nth-child(2)::before {
+  bottom: 0;
+  -webkit-transform: translate3d(0, -100%, 0);
+  transform: translate3d(0, -100%, 0);
+}
+
+.link--mallki:hover span::before {
+  -webkit-transition-delay: 0.3s;
+  transition-delay: 0.3s;
+  -webkit-transform: translate3d(0, 0, 0);
+  transform: translate3d(0, 0, 0);
+  -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+}
+</style>

+ 111 - 0
src/components/Tinymce/components/EditorImage.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="upload-container">
+    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
+      upload
+    </el-button>
+    <el-dialog :visible.sync="dialogVisible">
+      <el-upload
+        :multiple="true"
+        :file-list="fileList"
+        :show-file-list="true"
+        :on-remove="handleRemove"
+        :on-success="handleSuccess"
+        :before-upload="beforeUpload"
+        class="editor-slide-upload"
+        action="https://httpbin.org/post"
+        list-type="picture-card"
+      >
+        <el-button size="small" type="primary">
+          Click upload
+        </el-button>
+      </el-upload>
+      <el-button @click="dialogVisible = false">
+        Cancel
+      </el-button>
+      <el-button type="primary" @click="handleSubmit">
+        Confirm
+      </el-button>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+// import { getToken } from 'api/qiniu'
+
+export default {
+  name: 'EditorSlideUpload',
+  props: {
+    color: {
+      type: String,
+      default: '#1890ff'
+    }
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      listObj: {},
+      fileList: []
+    }
+  },
+  methods: {
+    checkAllSuccess() {
+      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
+    },
+    handleSubmit() {
+      const arr = Object.keys(this.listObj).map(v => this.listObj[v])
+      if (!this.checkAllSuccess()) {
+        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
+        return
+      }
+      this.$emit('successCBK', arr)
+      this.listObj = {}
+      this.fileList = []
+      this.dialogVisible = false
+    },
+    handleSuccess(response, file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          this.listObj[objKeyArr[i]].url = response.files.file
+          this.listObj[objKeyArr[i]].hasSuccess = true
+          return
+        }
+      }
+    },
+    handleRemove(file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          delete this.listObj[objKeyArr[i]]
+          return
+        }
+      }
+    },
+    beforeUpload(file) {
+      const _self = this
+      const _URL = window.URL || window.webkitURL
+      const fileName = file.uid
+      this.listObj[fileName] = {}
+      return new Promise((resolve, reject) => {
+        const img = new Image()
+        img.src = _URL.createObjectURL(file)
+        img.onload = function() {
+          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
+        }
+        resolve(true)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.editor-slide-upload {
+  margin-bottom: 20px;
+  /deep/ .el-upload--picture-card {
+    width: 100%;
+  }
+}
+</style>

+ 59 - 0
src/components/Tinymce/dynamicLoadScript.js

@@ -0,0 +1,59 @@
+let callbacks = []
+
+function loadedTinymce() {
+  // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
+  // check is successfully downloaded script
+  return window.tinymce
+}
+
+const dynamicLoadScript = (src, callback) => {
+  const existingScript = document.getElementById(src)
+  const cb = callback || function() {}
+
+  if (!existingScript) {
+    const script = document.createElement('script')
+    script.src = src // src url for the third-party library being loaded.
+    script.id = src
+    document.body.appendChild(script)
+    callbacks.push(cb)
+    const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
+    onEnd(script)
+  }
+
+  if (existingScript && cb) {
+    if (loadedTinymce()) {
+      cb(null, existingScript)
+    } else {
+      callbacks.push(cb)
+    }
+  }
+
+  function stdOnEnd(script) {
+    script.onload = function() {
+      // this.onload = null here is necessary
+      // because even IE9 works not like others
+      this.onerror = this.onload = null
+      for (const cb of callbacks) {
+        cb(null, script)
+      }
+      callbacks = null
+    }
+    script.onerror = function() {
+      this.onerror = this.onload = null
+      cb(new Error('Failed to load ' + src), script)
+    }
+  }
+
+  function ieOnEnd(script) {
+    script.onreadystatechange = function() {
+      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
+      this.onreadystatechange = null
+      for (const cb of callbacks) {
+        cb(null, script) // there is no way to catch loading errors in IE8
+      }
+      callbacks = null
+    }
+  }
+}
+
+export default dynamicLoadScript

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

@@ -0,0 +1,237 @@
+<template>
+  <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
+    <textarea :id="tinymceId" class="tinymce-textarea" />
+    <div class="editor-custom-btn-container">
+      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
+    </div>
+  </div>
+</template>
+
+<script>
+/**
+ * docs:
+ * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
+ */
+import editorImage from './components/EditorImage'
+import plugins from './plugins'
+import toolbar from './toolbar'
+import load from './dynamicLoadScript'
+
+// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
+const tinymceCDN = 'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js'
+
+export default {
+  name: 'Tinymce',
+  components: { editorImage },
+  props: {
+    id: {
+      type: String,
+      default: function() {
+        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    toolbar: {
+      type: Array,
+      required: false,
+      default() {
+        return []
+      }
+    },
+    menubar: {
+      type: String,
+      default: 'file edit insert view format table'
+    },
+    height: {
+      type: [Number, String],
+      required: false,
+      default: 360
+    },
+    width: {
+      type: [Number, String],
+      required: false,
+      default: 'auto'
+    }
+  },
+  data() {
+    return {
+      hasChange: false,
+      hasInit: false,
+      tinymceId: this.id,
+      fullscreen: false,
+      languageTypeList: {
+        'en': 'en',
+        'zh': 'zh_CN',
+        'es': 'es_MX',
+        'ja': 'ja'
+      }
+    }
+  },
+  computed: {
+    containerWidth() {
+      const width = this.width
+      if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
+        return `${width}px`
+      }
+      return width
+    }
+  },
+  watch: {
+    value(val) {
+      if (!this.hasChange && this.hasInit) {
+        this.$nextTick(() =>
+          window.tinymce.get(this.tinymceId).setContent(val || ''))
+      }
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  activated() {
+    if (window.tinymce) {
+      this.initTinymce()
+    }
+  },
+  deactivated() {
+    this.destroyTinymce()
+  },
+  destroyed() {
+    this.destroyTinymce()
+  },
+  methods: {
+    init() {
+      // dynamic load tinymce from cdn
+      load(tinymceCDN, (err) => {
+        if (err) {
+          this.$message.error(err.message)
+          return
+        }
+        this.initTinymce()
+      })
+    },
+    initTinymce() {
+      const _this = this
+      window.tinymce.init({
+        selector: `#${this.tinymceId}`,
+        language: this.languageTypeList['en'],
+        height: this.height,
+        body_class: 'panel-body ',
+        object_resizing: false,
+        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
+        menubar: this.menubar,
+        plugins: plugins,
+        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',
+        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
+        default_link_target: '_blank',
+        link_title: false,
+        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
+        init_instance_callback: editor => {
+          if (_this.value) {
+            editor.setContent(_this.value)
+          }
+          _this.hasInit = true
+          editor.on('NodeChange Change KeyUp SetContent', () => {
+            this.hasChange = true
+            this.$emit('input', editor.getContent())
+          })
+        },
+        setup(editor) {
+          editor.on('FullscreenStateChanged', (e) => {
+            _this.fullscreen = e.state
+          })
+        }
+        // 整合七牛上传
+        // 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);
+        //   });
+        // },
+      })
+    },
+    destroyTinymce() {
+      const tinymce = window.tinymce.get(this.tinymceId)
+      if (this.fullscreen) {
+        tinymce.execCommand('mceFullScreen')
+      }
+
+      if (tinymce) {
+        tinymce.destroy()
+      }
+    },
+    setContent(value) {
+      window.tinymce.get(this.tinymceId).setContent(value)
+    },
+    getContent() {
+      window.tinymce.get(this.tinymceId).getContent()
+    },
+    imageSuccessCBK(arr) {
+      const _this = this
+      arr.forEach(v => {
+        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.tinymce-container {
+  position: relative;
+  line-height: normal;
+}
+.tinymce-container>>>.mce-fullscreen {
+  z-index: 10000;
+}
+.tinymce-textarea {
+  visibility: hidden;
+  z-index: -1;
+}
+.editor-custom-btn-container {
+  position: absolute;
+  right: 4px;
+  top: 4px;
+  /*z-index: 2005;*/
+}
+.fullscreen .editor-custom-btn-container {
+  z-index: 10000;
+  position: fixed;
+}
+.editor-upload-btn {
+  display: inline-block;
+}
+</style>

+ 7 - 0
src/components/Tinymce/plugins.js

@@ -0,0 +1,7 @@
+// Any plugins you want to use has to be imported
+// Detail plugins list see https://www.tinymce.com/docs/plugins/
+// Custom builds see https://www.tinymce.com/download/custom-builds/
+
+const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
+
+export default plugins

+ 6 - 0
src/components/Tinymce/toolbar.js

@@ -0,0 +1,6 @@
+// Here is a list of the toolbar
+// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
+
+const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
+
+export default toolbar

+ 356 - 0
src/components/TreeSelect/index.vue

@@ -0,0 +1,356 @@
+<!--
+    /**
+     * 树形下拉选择组件,下拉框展示树形结构,提供选择某节点功能,方便其他模块调用
+     * @author ljn
+     * @date 2019-04-02
+     * 调用示例:
+     * <tree-select :height="400" // 下拉框中树形高度
+     *              :width="200" // 下拉框中树形宽度
+     *              size="small"  // 输入框的尺寸: medium/small/mini
+     *              :data="data" // 树结构的数据
+     *              :defaultProps="defaultProps" // 树结构的props
+     *              multiple   // 多选
+     *              disabled //禁止
+     *              onlyleaf //单选时仅叶子可选
+     *              clearable   // 可清空选择
+     *              collapseTags   // 多选时将选中值按文字的形式展示
+     *              checkStrictly // 多选时,严格遵循父子不互相关联
+     *              :nodeKey="nodeKey"   // 绑定nodeKey,默认绑定'id'
+     *              :checkedKeys="defaultCheckedKeys"  // 传递默认选中的节点key组成的数组
+     *              @popoverHide="popoverHide"> // 事件有两个参数:第一个是所有选中的节点ID,第二个是所有选中的节点数据
+     *              </tree-select>
+     */
+-->
+<template>
+  <div>
+    <div v-show="isShowSelect" class="mask" @click="isShowSelect = !isShowSelect" />
+    <el-popover
+      v-model="isShowSelect"
+      :disabled="disabled"
+      placement="bottom-start"
+      :width="width"
+      trigger="manual"
+      @hide="popoverHide"
+    >
+      <el-tree
+        ref="tree"
+        class="common-tree"
+        :style="style"
+        :data="data"
+        :props="defaultProps"
+        :show-checkbox="multiple"
+        :node-key="nodeKey"
+        :disabled="disabled"
+        :check-strictly="checkStrictly"
+        :default-expand-all="expandAll"
+        :expand-on-click-node="false"
+        :check-on-click-node="multiple"
+        :highlight-current="true"
+        @node-click="handleNodeClick"
+        @check-change="handleCheckChange"
+      />
+      <el-select
+        slot="reference"
+        ref="select"
+        v-model="selectedData"
+        :size="size"
+        :style="selectStyle"
+        :disabled="disabled"
+        :placeholder="placeholder"
+        :multiple="multiple"
+        :clearable="clearable"
+        :collapse-tags="collapseTags"
+        class="tree-select"
+        @click.native="isShowSelect = !isShowSelect"
+        @remove-tag="removeSelectedNodes"
+        @clear="removeSelectedNode"
+        @change="changeSelectedNodes"
+      >
+        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+      </el-select>
+    </el-popover>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TreeSelect',
+  props: {
+    // 树结构数据
+    data: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    defaultProps: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    // 配置是否可多选
+    multiple: {
+      type: Boolean,
+      default() {
+        return false
+      }
+    },
+    // 配置是否多选叶子
+    onlyleaf: {
+      type: Boolean,
+      default() {
+        return false
+      }
+    },
+    // 配置是否可清空选择
+    clearable: {
+      type: Boolean,
+      default() {
+        return false
+      }
+    },
+    disabled: {
+      type: Boolean,
+      default() {
+        return true
+      }
+    },
+    placeholder: {
+      type: String,
+      default() {
+        return '请选择'
+      }
+    },
+    // 配置多选时是否将选中值按文字的形式展示
+    collapseTags: {
+      type: Boolean,
+      default() {
+        return false
+      }
+    },
+    // 配置多选时是否将选中值按文字的形式展示
+    expandAll: {
+      type: Boolean,
+      default() {
+        return false
+      }
+    },
+    nodeKey: {
+      type: String,
+      default() {
+        return 'id'
+      }
+    },
+    // 显示复选框情况下,是否严格遵循父子不互相关联
+    checkStrictly: {
+      type: Boolean,
+      default() {
+        return false
+      }
+    },
+    // 默认选中的节点key数组
+    checkedKeys: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    size: {
+      type: String,
+      default() {
+        return 'small'
+      }
+    },
+    width: {
+      type: Number,
+      default() {
+        return 250
+      }
+    },
+    display: {
+      type: String,
+      default() {
+        return 'block'
+      }
+    },
+    height: {
+      type: Number,
+      default() {
+        return 300
+      }
+    }
+  },
+  data() {
+    return {
+      isShowSelect: false, // 是否显示树状选择器
+      options: [],
+      selectedData: [], // 选中的节点
+      style: 'width:' + this.width + 'px;' + 'height:' + this.height + 'px;',
+      selectStyle: 'width:' + (this.width + 24) + 'px;' + 'display:' + this.display,
+      checkedIds: [],
+      checkedData: []
+    }
+  },
+  watch: {
+    isShowSelect(val) {
+      // 隐藏select自带的下拉框
+      this.$refs.select.blur()
+    },
+    checkedKeys(val) {
+      if (!val) return
+      this.checkedKeys = val
+      this.initCheckedData()
+    }
+  },
+  mounted() {
+    this.initCheckedData()
+  },
+  methods: {
+    // 单选时点击tree节点,设置select选项
+    setSelectOption(node) {
+      const tmpMap = {}
+      tmpMap.value = node.key
+      tmpMap.label = node.label
+      this.options = []
+      this.options.push(tmpMap)
+      this.selectedData = node.key
+    },
+    // 单选,选中传进来的节点
+    checkSelectedNode(checkedKeys) {
+      var item = checkedKeys[0]
+      this.$refs.tree.setCurrentKey(item)
+      var node = this.$refs.tree.getNode(item)
+      this.setSelectOption(node)
+    },
+    // 多选,勾选上传进来的节点
+    checkSelectedNodes(checkedKeys) {
+      this.$refs.tree.setCheckedKeys(checkedKeys)
+    },
+    // 单选,清空选中
+    clearSelectedNode() {
+      this.selectedData = ''
+      this.$refs.tree.setCurrentKey(null)
+    },
+    // 多选,清空所有勾选
+    clearSelectedNodes() {
+      var checkedKeys = this.$refs.tree.getCheckedKeys() // 所有被选中的节点的 key 所组成的数组数据
+      for (let i = 0; i < checkedKeys.length; i++) {
+        this.$refs.tree.setChecked(checkedKeys[i], false)
+      }
+    },
+    initCheckedData() {
+      if (this.multiple) {
+        // 多选
+        if (this.checkedKeys.length > 0) {
+          this.checkSelectedNodes(this.checkedKeys)
+        } else {
+          this.clearSelectedNodes()
+        }
+      } else {
+        // 单选
+        if (this.checkedKeys.length > 0) {
+          this.checkSelectedNode(this.checkedKeys)
+        } else {
+          this.clearSelectedNode()
+        }
+      }
+    },
+    popoverHide() {
+      if (this.multiple) {
+        this.checkedIds = this.$refs.tree.getCheckedKeys() // 所有被选中的节点的 key 所组成的数组数据
+        this.checkedData = this.$refs.tree.getCheckedNodes() // 所有被选中的节点所组成的数组数据
+      } else {
+        this.checkedIds = this.$refs.tree.getCurrentKey()
+        this.checkedData = this.$refs.tree.getCurrentNode()
+      }
+      this.$emit('popoverHide', this.checkedIds, this.checkedData)
+    },
+    // 单选,节点被点击时的回调,返回被点击的节点数据
+    handleNodeClick(data, node) {
+      if (!this.onlyleaf || (this.onlyleaf && (data.haschildren === 0 || data.children == undefined))) {
+        if (!this.multiple) {
+          this.setSelectOption(node)
+          this.isShowSelect = !this.isShowSelect
+          this.$emit('change', this.selectedData)
+        }
+      }
+    },
+    // 多选,节点勾选状态发生变化时的回调
+    handleCheckChange() {
+      var checkedKeys = this.$refs.tree.getCheckedKeys() // 所有被选中的节点的 key 所组成的数组数据
+      this.options = checkedKeys.map((item) => {
+        var node = this.$refs.tree.getNode(item) // 所有被选中的节点对应的node
+        const tmpMap = {}
+        tmpMap.value = node.key
+        tmpMap.label = node.label
+        return tmpMap
+      })
+      this.selectedData = this.options.map((item) => {
+        return item.value
+      })
+      this.$emit('change', this.selectedData)
+    },
+    // 多选,删除任一select选项的回调
+    removeSelectedNodes(val) {
+      this.$refs.tree.setChecked(val, false)
+      var node = this.$refs.tree.getNode(val)
+      if (!this.checkStrictly && node.childNodes.length > 0) {
+        this.treeToList(node).map(item => {
+          if (item.childNodes.length <= 0) {
+            this.$refs.tree.setChecked(item, false)
+          }
+        })
+        this.handleCheckChange()
+      }
+      this.$emit('change', this.selectedData)
+    },
+    treeToList(tree) {
+      var queen = []
+      var out = []
+      queen = queen.concat(tree)
+      while (queen.length) {
+        var first = queen.shift()
+        if (first.childNodes) {
+          queen = queen.concat(first.childNodes)
+        }
+        out.push(first)
+      }
+      return out
+    },
+    // 单选,清空select输入框的回调
+    removeSelectedNode() {
+      this.clearSelectedNode()
+      this.$emit('change', this.selectedData)
+      this.popoverHide()
+    },
+    // 选中的select选项改变的回调
+    changeSelectedNodes(selectedData) {
+      // 多选,清空select输入框时,清除树勾选
+      if (this.multiple && selectedData.length <= 0) {
+        this.clearSelectedNodes()
+      }
+      this.$emit('change', this.selectedData)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .mask{
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    opacity: 0;
+    z-index: 11;
+  }
+  .common-tree{
+    overflow: auto;
+  }
+
+  /* .tree-select{
+    z-index: 111;
+  } */
+</style>

+ 134 - 0
src/components/Upload/SingleImage.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        将文件拖到此处,或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl+'?imageView2/1/w/200/h/200'">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess() {
+      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 lang="scss" scoped>
+    @import "~@/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>

+ 130 - 0
src/components/Upload/SingleImage2.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="singleImageUpload2 upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        Drag或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div v-show="imageUrl.length>0" class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload2',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess() {
+      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(() => {
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style 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>

+ 157 - 0
src/components/Upload/SingleImage3.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        将文件拖到此处,或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div class="image-preview image-app-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+    <div class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload3',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess(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 lang="scss" scoped>
+@import "~@/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>

+ 138 - 0
src/components/UploadExcel/index.vue

@@ -0,0 +1,138 @@
+<template>
+  <div>
+    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
+    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
+      Drop excel file here or
+      <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
+        Browse
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import XLSX from 'xlsx'
+
+export default {
+  props: {
+    beforeUpload: Function, // eslint-disable-line
+    onSuccess: Function// eslint-disable-line
+  },
+  data() {
+    return {
+      loading: false,
+      excelData: {
+        header: null,
+        results: null
+      }
+    }
+  },
+  methods: {
+    generateData({ header, results }) {
+      this.excelData.header = header
+      this.excelData.results = results
+      this.onSuccess && this.onSuccess(this.excelData)
+    },
+    handleDrop(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      if (this.loading) return
+      const files = e.dataTransfer.files
+      if (files.length !== 1) {
+        this.$message.error('Only support uploading one file!')
+        return
+      }
+      const rawFile = files[0] // only use files[0]
+
+      if (!this.isExcel(rawFile)) {
+        this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
+        return false
+      }
+      this.upload(rawFile)
+      e.stopPropagation()
+      e.preventDefault()
+    },
+    handleDragover(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      e.dataTransfer.dropEffect = 'copy'
+    },
+    handleUpload() {
+      this.$refs['excel-upload-input'].click()
+    },
+    handleClick(e) {
+      const files = e.target.files
+      const rawFile = files[0] // only use files[0]
+      if (!rawFile) return
+      this.upload(rawFile)
+    },
+    upload(rawFile) {
+      this.$refs['excel-upload-input'].value = null // fix can't select the same excel
+
+      if (!this.beforeUpload) {
+        this.readerData(rawFile)
+        return
+      }
+      const before = this.beforeUpload(rawFile)
+      if (before) {
+        this.readerData(rawFile)
+      }
+    },
+    readerData(rawFile) {
+      this.loading = true
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = e => {
+          const data = e.target.result
+          const workbook = XLSX.read(data, { type: 'array' })
+          const firstSheetName = workbook.SheetNames[0]
+          const worksheet = workbook.Sheets[firstSheetName]
+          const header = this.getHeaderRow(worksheet)
+          const results = XLSX.utils.sheet_to_json(worksheet)
+          this.generateData({ header, results })
+          this.loading = false
+          resolve()
+        }
+        reader.readAsArrayBuffer(rawFile)
+      })
+    },
+    getHeaderRow(sheet) {
+      const headers = []
+      const range = XLSX.utils.decode_range(sheet['!ref'])
+      let C
+      const R = range.s.r
+      /* start in the first row */
+      for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
+        const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
+        /* find the cell in the first row */
+        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
+        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
+        headers.push(hdr)
+      }
+      return headers
+    },
+    isExcel(file) {
+      return /\.(xlsx|xls|csv)$/.test(file.name)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.excel-upload-input{
+  display: none;
+  z-index: -9999;
+}
+.drop{
+  border: 2px dashed #bbb;
+  width: 600px;
+  height: 160px;
+  line-height: 160px;
+  margin: 0 auto;
+  font-size: 24px;
+  border-radius: 5px;
+  text-align: center;
+  color: #bbb;
+  position: relative;
+}
+</style>

+ 114 - 0
src/data.js

@@ -0,0 +1,114 @@
+const data = [
+  {
+    pasture: '一级分类',
+    pasture1: '二级分类',
+    pasture2: '三级分类',
+    type: '类型',
+    pastureName1: '宝鸡1',
+    pastureName2: '宝鸡2'
+  },
+  // 车辆A
+  {
+    pasture: '车辆A',
+    pasture1: '装载机A',
+    pasture2: '小型装载机A',
+    type: '数量',
+    barCode: '323',
+    pastureName1: '1111',
+    pastureName2: '2222'
+  },
+  {
+    pasture: '车辆A',
+    pasture1: '装载机A',
+    pasture2: '小型装载机A',
+    type: '运转率',
+    pastureName1: '1111',
+    pastureName2: '2222'
+  },
+  {
+    pasture: '车辆A',
+    pasture1: '装载机A',
+    pasture2: '小型装载机A',
+    type: '闲置率',
+    pastureName1: '1111',
+    pastureName2: '2222'
+  },
+  {
+    pasture: '车辆A',
+    pasture1: '装载机A',
+    pasture2: '小型装载机B',
+    type: '数量',
+    barCode: '323',
+    pastureName1: '1111',
+    pastureName2: '2222'
+  },
+  {
+    pasture: '车辆A',
+    pasture1: '装载机A',
+    pasture2: '小型装载机B',
+    type: '运转率',
+    pastureName1: '1111',
+    pastureName2: '2222'
+  },
+  {
+    pasture: '车辆A',
+    pasture1: '装载机A',
+    pasture2: '小型装载机B',
+    type: '闲置率',
+    pastureName1: '1111',
+    pastureName2: '2222'
+  },
+  // 车辆B
+  {
+    pasture: '车辆B',
+    pasture1: '装载机B',
+    pasture2: '小型装载机A',
+    type: '数量',
+    barCode: '323',
+    pastureName1: '2222',
+    pastureName2: '3333'
+  },
+  {
+    pasture: '车辆B',
+    pasture1: '装载机B',
+    pasture2: '小型装载机A',
+    type: '运转率',
+    pastureName1: '2222',
+    pastureName2: '3333'
+  },
+  {
+    pasture: '车辆B',
+    pasture1: '装载机B',
+    pasture2: '小型装载机A',
+    type: '闲置率',
+    pastureName1: '2222',
+    pastureName2: '3333'
+  },
+  {
+    pasture: '车辆B',
+    pasture1: '装载机B',
+    pasture2: '小型装载机B',
+    type: '数量',
+    barCode: '323',
+    pastureName1: '2222',
+    pastureName2: '3333'
+  },
+  {
+    pasture: '车辆B',
+    pasture1: '装载机B',
+    pasture2: '小型装载机B',
+    type: '运转率',
+    pastureName1: '2222',
+    pastureName2: '3333'
+  },
+  {
+    pasture: '车辆B',
+    pasture1: '装载机B',
+    pasture2: '小型装载机B',
+    type: '闲置率',
+    pastureName1: '2222',
+    pastureName2: '3333'
+  }
+]
+
+export default data

+ 49 - 0
src/directive/clipboard/clipboard.js

@@ -0,0 +1,49 @@
+// Inspired by https://github.com/Inndy/vue-clipboard2
+const Clipboard = require('clipboard')
+if (!Clipboard) {
+  throw new Error('you should npm install `clipboard` --save at first ')
+}
+
+export default {
+  bind(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      const clipboard = new Clipboard(el, {
+        text() { return binding.value },
+        action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+      })
+      clipboard.on('success', e => {
+        const callback = el._v_clipboard_success
+        callback && callback(e) // eslint-disable-line
+      })
+      clipboard.on('error', e => {
+        const callback = el._v_clipboard_error
+        callback && callback(e) // eslint-disable-line
+      })
+      el._v_clipboard = clipboard
+    }
+  },
+  update(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      el._v_clipboard.text = function() { return binding.value }
+      el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+    }
+  },
+  unbind(el, binding) {
+    if (binding.arg === 'success') {
+      delete el._v_clipboard_success
+    } else if (binding.arg === 'error') {
+      delete el._v_clipboard_error
+    } else {
+      el._v_clipboard.destroy()
+      delete el._v_clipboard
+    }
+  }
+}

+ 13 - 0
src/directive/clipboard/index.js

@@ -0,0 +1,13 @@
+import Clipboard from './clipboard'
+
+const install = function(Vue) {
+  Vue.directive('Clipboard', Clipboard)
+}
+
+if (window.Vue) {
+  window.clipboard = Clipboard
+  Vue.use(install); // eslint-disable-line
+}
+
+Clipboard.install = install
+export default Clipboard

+ 77 - 0
src/directive/el-drag-dialog/drag.js

@@ -0,0 +1,77 @@
+export default {
+  bind(el, binding, vnode) {
+    const dialogHeaderEl = el.querySelector('.el-dialog__header')
+    const dragDom = el.querySelector('.el-dialog')
+    dialogHeaderEl.style.cssText += ';cursor:move;'
+    dragDom.style.cssText += ';top:0px;'
+
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const getStyle = (function() {
+      if (window.document.currentStyle) {
+        return (dom, attr) => dom.currentStyle[attr]
+      } else {
+        return (dom, attr) => getComputedStyle(dom, false)[attr]
+      }
+    })()
+
+    dialogHeaderEl.onmousedown = (e) => {
+      // 鼠标按下,计算当前元素距离可视区的距离
+      const disX = e.clientX - dialogHeaderEl.offsetLeft
+      const disY = e.clientY - dialogHeaderEl.offsetTop
+
+      const dragDomWidth = dragDom.offsetWidth
+      const dragDomHeight = dragDom.offsetHeight
+
+      const screenWidth = document.body.clientWidth
+      const screenHeight = document.body.clientHeight
+
+      const minDragDomLeft = dragDom.offsetLeft
+      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
+
+      const minDragDomTop = dragDom.offsetTop
+      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
+
+      // 获取到的值带px 正则匹配替换
+      let styL = getStyle(dragDom, 'left')
+      let styT = getStyle(dragDom, 'top')
+
+      if (styL.includes('%')) {
+        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
+        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
+      } else {
+        styL = +styL.replace(/\px/g, '')
+        styT = +styT.replace(/\px/g, '')
+      }
+
+      document.onmousemove = function(e) {
+        // 通过事件委托,计算移动的距离
+        let left = e.clientX - disX
+        let top = e.clientY - disY
+
+        // 边界处理
+        if (-(left) > minDragDomLeft) {
+          left = -minDragDomLeft
+        } else if (left > maxDragDomLeft) {
+          left = maxDragDomLeft
+        }
+
+        if (-(top) > minDragDomTop) {
+          top = -minDragDomTop
+        } else if (top > maxDragDomTop) {
+          top = maxDragDomTop
+        }
+
+        // 移动当前元素
+        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
+
+        // emit onDrag event
+        vnode.child.$emit('dragDialog')
+      }
+
+      document.onmouseup = function(e) {
+        document.onmousemove = null
+        document.onmouseup = null
+      }
+    }
+  }
+}

+ 13 - 0
src/directive/el-drag-dialog/index.js

@@ -0,0 +1,13 @@
+import drag from './drag'
+
+const install = function(Vue) {
+  Vue.directive('el-drag-dialog', drag)
+}
+
+if (window.Vue) {
+  window['el-drag-dialog'] = drag
+  Vue.use(install); // eslint-disable-line
+}
+
+drag.install = install
+export default drag

+ 41 - 0
src/directive/el-table/adaptive.js

@@ -0,0 +1,41 @@
+import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
+
+/**
+ * How to use
+ * <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
+ * el-table height is must be set
+ * bottomOffset: 30(default)   // The height of the table from the bottom of the page.
+ */
+
+const doResize = (el, binding, vnode) => {
+  const { componentInstance: $table } = vnode
+
+  const { value } = binding
+
+  if (!$table.height) {
+    throw new Error(`el-$table must set the height. Such as height='100px'`)
+  }
+  const bottomOffset = (value && value.bottomOffset) || 30
+
+  if (!$table) return
+
+  const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
+  $table.layout.setHeight(height)
+  $table.doLayout()
+}
+
+export default {
+  bind(el, binding, vnode) {
+    el.resizeListener = () => {
+      doResize(el, binding, vnode)
+    }
+    // parameter 1 is must be "Element" type
+    addResizeListener(window.document.body, el.resizeListener)
+  },
+  inserted(el, binding, vnode) {
+    doResize(el, binding, vnode)
+  },
+  unbind(el) {
+    removeResizeListener(window.document.body, el.resizeListener)
+  }
+}

+ 13 - 0
src/directive/el-table/index.js

@@ -0,0 +1,13 @@
+import adaptive from './adaptive'
+
+const install = function(Vue) {
+  Vue.directive('el-height-adaptive-table', adaptive)
+}
+
+if (window.Vue) {
+  window['el-height-adaptive-table'] = adaptive
+  Vue.use(install); // eslint-disable-line
+}
+
+adaptive.install = install
+export default adaptive

+ 30 - 0
src/directive/enterToNext/enterToNext.js

@@ -0,0 +1,30 @@
+/**
+ * How to use
+ * el-dilalog,只需在el-form中指定v-enterToNext,后面的【=”true"】其实可能省略
+ * <el-from :model ="form" v-enterToNext="true">
+ * */
+
+export default {
+  inserted: function(el) {
+    // let frm = el.querySelector('.el-form');
+    const inputs = el.querySelectorAll('input')
+    // 绑定回写事件
+    for (var i = 0; i < inputs.length; i++) {
+      inputs[i].setAttribute('keyfocusindex', i)
+      inputs[i].addEventListener('keyup', (ev) => {
+        if (ev.keyCode === 13) {
+          const targetTo = ev.srcElement.getAttribute('keyfocusto')
+          console.log(ev.srcElement)
+          if (targetTo) {
+            var ctlJ = parseInt(targetTo)
+            if (ctlJ < inputs.length) { inputs[ctlJ].focus() }
+          } else {
+            var attrIndex = ev.srcElement.getAttribute('keyfocusindex')
+            var ctlI = parseInt(attrIndex)
+            if (ctlI < inputs.length - 1) { inputs[ctlI + 1].focus() }
+          }
+        }
+      })
+    }
+  }
+}

+ 13 - 0
src/directive/enterToNext/index.js

@@ -0,0 +1,13 @@
+import enterToNext from './enterToNext'
+
+const install = function(Vue) {
+  Vue.directive('enterToNext', enterToNext)
+}
+
+if (window.Vue) {
+  window['enterToNext'] = enterToNext
+  Vue.use(install); // eslint-disable-line
+}
+
+enterToNext.install = install
+export default enterToNext

+ 13 - 0
src/directive/permission/index.js

@@ -0,0 +1,13 @@
+import permission from './permission'
+
+const install = function(Vue) {
+  Vue.directive('permission', permission)
+}
+
+if (window.Vue) {
+  window['permission'] = permission
+  Vue.use(install); // eslint-disable-line
+}
+
+permission.install = install
+export default permission

+ 22 - 0
src/directive/permission/permission.js

@@ -0,0 +1,22 @@
+import store from '@/store'
+
+export default {
+  inserted(el, binding, vnode) {
+    const { value } = binding
+    const roles = store.getters && store.getters.roles
+
+    if (value && value instanceof Array && value.length > 0) {
+      const permissionRoles = value
+
+      const hasPermission = roles.some(role => {
+        return permissionRoles.includes(role)
+      })
+
+      if (!hasPermission) {
+        el.parentNode && el.parentNode.removeChild(el)
+      }
+    } else {
+      throw new Error(`need roles! Like v-permission="['admin','editor']"`)
+    }
+  }
+}

+ 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 || {}
+      const stickyTop = params.stickyTop || 0
+      const zIndex = params.zIndex || 1000
+      const 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
+

+ 13 - 0
src/directive/waves/index.js

@@ -0,0 +1,13 @@
+import waves from './waves'
+
+const install = function(Vue) {
+  Vue.directive('waves', waves)
+}
+
+if (window.Vue) {
+  window.waves = waves
+  Vue.use(install); // eslint-disable-line
+}
+
+waves.install = install
+export default waves

+ 26 - 0
src/directive/waves/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;
+}

+ 72 - 0
src/directive/waves/waves.js

@@ -0,0 +1,72 @@
+import './waves.css'
+
+const context = '@@wavesContext'
+
+function handleClick(el, binding) {
+  function handle(e) {
+    const customOpts = Object.assign({}, binding.value)
+    const opts = Object.assign({
+      ele: el, // 波纹作用元素
+      type: 'hit', // hit 点击位置扩散 center中心点扩展
+      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+    },
+    customOpts
+    )
+    const 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.documentElement.scrollTop ||
+              document.body.scrollTop) + 'px'
+          ripple.style.left =
+            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
+              document.body.scrollLeft) + 'px'
+      }
+      ripple.style.backgroundColor = opts.color
+      ripple.className = 'waves-ripple z-active'
+      return false
+    }
+  }
+
+  if (!el[context]) {
+    el[context] = {
+      removeHandle: handle
+    }
+  } else {
+    el[context].removeHandle = handle
+  }
+
+  return handle
+}
+
+export default {
+  bind(el, binding) {
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  update(el, binding) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  unbind(el) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el[context] = null
+    delete el[context]
+  }
+}

+ 68 - 0
src/filters/index.js

@@ -0,0 +1,68 @@
+// import parseTime, formatTime and set to filter
+export { parseTime, formatTime } from '@/utils'
+
+/**
+ * Show plural label if time is plural number
+ * @param {number} time
+ * @param {string} label
+ * @return {string}
+ */
+function pluralize(time, label) {
+  if (time === 1) {
+    return time + label
+  }
+  return time + label + 's'
+}
+
+/**
+ * @param {number} time
+ */
+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')
+  }
+}
+
+/**
+ * Number formatting
+ * like 10000 => 10k
+ * @param {number} num
+ * @param {number} digits
+ */
+export function numberFormatter(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()
+}
+
+/**
+ * 10000 => "10,000"
+ * @param {number} num
+ */
+export function toThousandFilter(num) {
+  return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
+}
+
+/**
+ * Upper case first char
+ * @param {String} string
+ */
+export function uppercaseFirst(string) {
+  return string.charAt(0).toUpperCase() + string.slice(1)
+}

+ 9 - 0
src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+requireAll(req)

File diff suppressed because it is too large
+ 0 - 0
src/icons/svg/SQL管理.svg


File diff suppressed because it is too large
+ 0 - 0
src/icons/svg/dashboard.svg


+ 1 - 0
src/icons/svg/elec.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575274299212" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4372" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 64H128c-19.2 0-32 12.8-32 32v832c0 19.2 12.8 32 32 32h768c19.2 0 32-12.8 32-32V96c0-19.2-12.8-32-32-32z m-236.8 556.8l-156.8 240c-3.2 6.4-9.6 6.4-16 3.2s-6.4-9.6-6.4-12.8l22.4-131.2c3.2-12.8-6.4-22.4-19.2-25.6H480l-92.8-3.2c-12.8 0-22.4-9.6-22.4-22.4 0-3.2 0-6.4 3.2-9.6l124.8-201.6c3.2-6.4 9.6-6.4 16-3.2 3.2 3.2 6.4 6.4 6.4 9.6l-9.6 102.4c0 12.8 6.4 22.4 19.2 22.4H640c12.8 0 22.4 9.6 22.4 22.4 0 0-3.2 6.4-3.2 9.6zM832 352H192V192c0-19.2 12.8-32 32-32h576c19.2 0 32 12.8 32 32v160z" p-id="4373"></path></svg>

+ 1 - 0
src/icons/svg/example.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

+ 1 - 0
src/icons/svg/exit-fullscreen.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>

+ 1 - 0
src/icons/svg/eye-open.svg

@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

Some files were not shown because too many files changed in this diff