Browse Source

Merge branch 'dev' into 'master'

Dev

See merge request tuosi-crowd-test-service/cofotest-frontend!8
郭超 3 years ago
parent
commit
9406482e68
100 changed files with 21428 additions and 69 deletions
  1. 1 1
      build/build.js
  2. 9 0
      build/webpack.base.conf.js
  3. 3 2
      config/dev.env.js
  4. 1 1
      config/index.js
  5. 2 1
      config/prod.env.js
  6. 1 1
      config/proxyConfig.js
  7. 11 3
      config/test.env.js
  8. 17852 1
      package-lock.json
  9. 6 2
      package.json
  10. 11 14
      src/App.vue
  11. 65 0
      src/components/DragSelect/index.vue
  12. 101 0
      src/components/Pagination/index.vue
  13. 62 0
      src/components/SvgIcon/index.vue
  14. 113 0
      src/components/TextHoverEffect/Mallki.vue
  15. 111 0
      src/components/file/FileUpload.vue
  16. 103 0
      src/components/file/ImgUpload.vue
  17. 75 0
      src/components/project/ProjectSearch.vue
  18. 87 0
      src/components/project/TaskSearch.vue
  19. 38 8
      src/components/task/Task.vue
  20. 63 0
      src/components/text/ExpendText.vue
  21. 7 3
      src/config/index.js
  22. 49 0
      src/directive/clipboard/clipboard.js
  23. 13 0
      src/directive/clipboard/index.js
  24. 77 0
      src/directive/el-drag-dialog/drag.js
  25. 13 0
      src/directive/el-drag-dialog/index.js
  26. 41 0
      src/directive/el-table/adaptive.js
  27. 13 0
      src/directive/el-table/index.js
  28. 13 0
      src/directive/permission/index.js
  29. 31 0
      src/directive/permission/permission.js
  30. 91 0
      src/directive/sticky.js
  31. 13 0
      src/directive/waves/index.js
  32. 26 0
      src/directive/waves/waves.css
  33. 72 0
      src/directive/waves/waves.js
  34. 9 0
      src/icons/index.js
  35. 1 0
      src/icons/svg/404.svg
  36. 1 0
      src/icons/svg/bug.svg
  37. 1 0
      src/icons/svg/chart.svg
  38. 1 0
      src/icons/svg/clipboard.svg
  39. 1 0
      src/icons/svg/component.svg
  40. 0 0
      src/icons/svg/dashboard.svg
  41. 1 0
      src/icons/svg/documentation.svg
  42. 1 0
      src/icons/svg/drag.svg
  43. 1 0
      src/icons/svg/edit.svg
  44. 1 0
      src/icons/svg/education.svg
  45. 1 0
      src/icons/svg/email.svg
  46. 1 0
      src/icons/svg/example.svg
  47. 1 0
      src/icons/svg/excel.svg
  48. 1 0
      src/icons/svg/exit-fullscreen.svg
  49. 1 0
      src/icons/svg/eye-open.svg
  50. 1 0
      src/icons/svg/eye.svg
  51. 0 0
      src/icons/svg/form.svg
  52. 1 0
      src/icons/svg/fullscreen.svg
  53. 1 0
      src/icons/svg/guide.svg
  54. 1 0
      src/icons/svg/icon.svg
  55. 1 0
      src/icons/svg/international.svg
  56. 1 0
      src/icons/svg/language.svg
  57. 1 0
      src/icons/svg/link.svg
  58. 1 0
      src/icons/svg/list.svg
  59. 1 0
      src/icons/svg/lock.svg
  60. 1 0
      src/icons/svg/message.svg
  61. 1 0
      src/icons/svg/money.svg
  62. 1 0
      src/icons/svg/nested.svg
  63. 1 0
      src/icons/svg/password.svg
  64. 1 0
      src/icons/svg/pdf.svg
  65. 1 0
      src/icons/svg/people.svg
  66. 1 0
      src/icons/svg/peoples.svg
  67. 0 0
      src/icons/svg/qq.svg
  68. 1 0
      src/icons/svg/search.svg
  69. 0 0
      src/icons/svg/shopping.svg
  70. 1 0
      src/icons/svg/size.svg
  71. 1 0
      src/icons/svg/skill.svg
  72. 1 0
      src/icons/svg/star.svg
  73. 1 0
      src/icons/svg/tab.svg
  74. 1 0
      src/icons/svg/table.svg
  75. 1 0
      src/icons/svg/theme.svg
  76. 1 0
      src/icons/svg/tree-table.svg
  77. 1 0
      src/icons/svg/tree.svg
  78. 1 0
      src/icons/svg/user.svg
  79. 1 0
      src/icons/svg/wechat.svg
  80. 1 0
      src/icons/svg/zip.svg
  81. 22 0
      src/icons/svgo.yml
  82. 62 24
      src/js/api.js
  83. 5 0
      src/js/file.js
  84. 4 7
      src/js/http.js
  85. 5 1
      src/main.js
  86. 161 0
      src/pages/Statistics/TaskStatistics.vue
  87. 113 0
      src/pages/Statistics/components/BugLevelChart.vue
  88. 105 0
      src/pages/Statistics/components/BugTimeCountChart.vue
  89. 99 0
      src/pages/Statistics/components/BugTypeChart.vue
  90. 123 0
      src/pages/Statistics/components/InfoCard.vue
  91. 235 0
      src/pages/Statistics/components/PanelGroup.vue
  92. 236 0
      src/pages/Statistics/components/StaffDistributionMap/index.vue
  93. 77 0
      src/pages/Statistics/components/StaffDistributionMap/map-data.js
  94. 81 0
      src/pages/Statistics/components/TodoList/Todo.vue
  95. 320 0
      src/pages/Statistics/components/TodoList/index.scss
  96. 127 0
      src/pages/Statistics/components/TodoList/index.vue
  97. 57 0
      src/pages/Statistics/components/TransactionTable.vue
  98. 55 0
      src/pages/Statistics/components/mixins/resize.js
  99. 101 0
      src/pages/TestCase/components/defect_detail.vue
  100. 215 0
      src/pages/TestCase/components/defect_form.vue

+ 1 - 1
build/build.js

@@ -2,7 +2,7 @@
 require('./check-versions')()
 
 process.env.NODE_ENV = 'production'
-
+// process.env.env_config = 'test'
 const ora = require('ora')
 const rm = require('rimraf')
 const path = require('path')

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

@@ -55,6 +55,7 @@ module.exports = {
       {
         test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
         loader: 'url-loader',
+        exclude: [resolve('src/icons')],
         options: {
           limit: 10000,
           name: utils.assetsPath('img/[name].[hash:7].[ext]')
@@ -75,6 +76,14 @@ module.exports = {
           limit: 10000,
           name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
         }
+      },
+      {
+        test: /\.svg$/,
+        loader: 'svg-sprite-loader',
+        include: [resolve('src/icon')],
+        options: {
+          symbolId: 'icon-[name]'
+        }
       }
     ]
   },

+ 3 - 2
config/dev.env.js

@@ -14,7 +14,8 @@ const prodEnv = require('./prod.env')
 module.exports = {
   NODE_ENV: '"development"',
   ENV_CONFIG: "'dev'",
-  API_ROOT: '"//127.0.0.1"',
+  API_ROOT: '"http://127.0.0.1:5757"',
   LOGIN_URL: '"http://127.0.0.1:8081/page/login?redirect=http%3a%2f%2fcrowd.dev.mooctest.net%2f%23%2fhome"',
-  REGISTER_URL: '"http://127.0.0.1:8081/page/register"'
+  REGISTER_URL: '"http://127.0.0.1:8081/page/register"',
+  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/dev/"'
 }

+ 1 - 1
config/index.js

@@ -13,7 +13,7 @@ module.exports = {
     proxyTable: proxyTable,
 
     // Various Dev Server settings
-    host: 'localhost', // can be overwritten by process.env.HOST
+    host: '127.0.0.1', // can be overwritten by process.env.HOST
     port: 5757, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
     autoOpenBrowser: false,
     errorOverlay: true,

+ 2 - 1
config/prod.env.js

@@ -4,5 +4,6 @@ module.exports = {
   ENV_CONFIG: "'prod'",
   API_ROOT: '"//www.cofortest.com"',
   LOGIN_URL:'"http://user.cofortest.com/page/login?redirect=http%3a%2f%2fwww.cofortest.com%2f%23%2fhome"',
-  REGISTER_URL: '"http://user.cofortest.com/page/register"'
+  REGISTER_URL: '"http://user.cofortest.com/page/register"',
+  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/online/"'
 }

+ 1 - 1
config/proxyConfig.js

@@ -1,6 +1,6 @@
 module.exports = {
   '/api': {
-    target: 'http://localhost:8080/', // 设置你调用的接口域名和端口号
+    target: 'http://127.0.0.1:8080/api/', // 设置你调用的接口域名和端口号
     changeOrigin: true,     // 跨域
     pathRewrite: {
       '^/api': '/'

+ 11 - 3
config/test.env.js

@@ -1,8 +1,16 @@
 'use strict'
+// module.exports = {
+//   NODE_ENV: '"test"',
+//   ENV_CONFIG: "'test'",
+//   API_ROOT: '"//crowd.mooctest.net:8083"',
+//   LOGIN_URL:'"http://user.mooctest.net:8081/page/login?redirect=http%3a%2f%2fcrowd.mooctest.net:8083%2f%23%2fhome"',
+//   REGISTER_URL: '"http://user.mooctest.net:8081/page/register"'
+// }
 module.exports = {
   NODE_ENV: '"test"',
   ENV_CONFIG: "'test'",
-  API_ROOT: '"//crowd.mooctest.net:8083"',
-  LOGIN_URL:'"http://user.mooctest.net:8081/page/login?redirect=http%3a%2f%2fcrowd.mooctest.net:8083%2f%23%2fhome"',
-  REGISTER_URL: '"http://user.mooctest.net:8081/page/register"'
+  API_ROOT: '"http://10.18.18.39"',
+  // LOGIN_URL: '"http://10.18.18.39:8081/page/login?redirect=http%3a%2f%2fcrowd.dev.mooctest.net%2f%23%2fhome"',
+  // REGISTER_URL: '"http://10.18.18.39:8081/page/register"',
+  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/dev/"'
 }

File diff suppressed because it is too large
+ 17852 - 1
package-lock.json


+ 6 - 2
package.json

@@ -9,20 +9,24 @@
     "start": "npm run dev",
     "lint": "eslint --ext .js,.vue src",
     "build--dev": "NODE_ENV=production env_config=dev node build/build.js",
-    "build--test": "NODE_ENV=production env_config=test node build/build.js",
+    "build--test": "set NODE_ENV=production && set env_config=test && node build/build.js",
     "build--prod": "NODE_ENV=production env_config=prod node build/build.js"
   },
   "dependencies": {
     "axios": "^0.21.1",
     "echarts": "^4.9.0",
-    "echarts-wordcloud": "^1.1.3",
+    "echarts-wordcloud": "^2.0.0",
     "element-ui": "^2.14.1",
     "font-awesome": "^4.7.0",
     "mockjs": "^1.1.0",
     "moment": "^2.29.1",
     "querystring": "^0.2.0",
+    "sortablejs": "^1.14.0",
+    "svg-sprite-loader": "^6.0.11",
     "v-region": "^2.2.2",
     "vue": "^2.6.12",
+    "vue-count-to": "^1.0.13",
+    "vue-cute-timeline": "^1.2.10",
     "vue-router": "^3.4.9",
     "vue-waterfall": "^1.0.6",
     "vue-waterfall-easy": "^2.4.4",

+ 11 - 14
src/App.vue

@@ -6,7 +6,9 @@
     <div class="container-wrapper">
       <slot>
         <div class="main-container">
-          <router-view/>
+          <keep-alive>
+            <router-view/>
+          </keep-alive>
         </div>
       </slot>
     </div>
@@ -15,19 +17,16 @@
 </template>
 
 <script>
-import Http from '@/js/http.js'
 import HeaderContainer from '@/components/commons/Header2.0'
 import FooterContainer from '@/components/commons/Footer2.0'
 import HomeSlice from '@/components/commons/HomeSlice'
 import HomeSliceOnline from '@/components/commons/HomeSliceOnline'
-import {getCurrentUser, storageGet, storageSave} from '@/js/index'
-import {setConfig} from '../src/config/index'
-import {slice_type} from "../tool4deploy/slider-type";
+import {slice_type} from '../tool4deploy/slider-type'
 
 export default {
   name: 'App',
-  components: {HeaderContainer, FooterContainer, HomeSlice ,HomeSliceOnline},
-  data(){
+  components: {HeaderContainer, FooterContainer, HomeSlice, HomeSliceOnline},
+  data () {
     return {
       // showSlice:false
       slice_type
@@ -46,7 +45,6 @@ export default {
       //   this.setCurrUserByHttp()
       // })
 
-
       // if (storageGet('user') == null) {
       //   storageSave('rolesPermissions', {
       //     'isRegionManager': false,
@@ -73,14 +71,13 @@ export default {
       //   this.fullScreenLoading = false
       //   this.isLogin = true
       // }
-    },
-  },
-  computed:{
-    showSlice(){
-      if(this.$route.path==='/home' || this.$route.path==='/')
-        return  true;
     }
   },
+  computed: {
+    showSlice () {
+      if (this.$route.path === '/home' || this.$route.path === '/') { return true }
+    }
+  }
 }
 </script>
 

+ 65 - 0
src/components/DragSelect/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
+    <slot />
+  </el-select>
+</template>
+
+<script>
+import Sortable from 'sortablejs'
+
+export default {
+  name: 'DragSelect',
+  props: {
+    value: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    selectVal: {
+      get() {
+        return [...this.value]
+      },
+      set(val) {
+        this.$emit('input', [...val])
+      }
+    }
+  },
+  mounted() {
+    this.setSort()
+  },
+  methods: {
+    setSort() {
+      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
+      this.sortable = Sortable.create(el, {
+        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
+        setData: function(dataTransfer) {
+          dataTransfer.setData('Text', '')
+          // to avoid Firefox bug
+          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+        },
+        onEnd: evt => {
+          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
+          this.value.splice(evt.newIndex, 0, targetRow)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drag-select {
+  ::v-deep {
+    .sortable-ghost {
+      opacity: .8;
+      color: #fff !important;
+      background: #42b983 !important;
+    }
+
+    .el-tag {
+      cursor: pointer;
+    }
+  }
+}
+</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: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    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: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 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/file/FileUpload.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-upload
+    drag
+    class="upload-file"
+    :action="uploadUrl"
+    :on-success="handleUploadSuccess"
+    multiple
+    :limit="countLimit"
+    :on-exceed="handleExceed"
+    :before-upload="beforeFileUpload"
+    :on-remove="handleFileRemove"
+    :file-list="fileList"
+    :on-error="fileUploadError"
+    :disabled="disabled"
+  >
+    <i class="el-icon-upload"></i>
+    <div class="el-upload__text">
+      将文件拖到此处,或
+      <em>点击上传</em>
+    </div>
+  </el-upload>
+</template>
+
+<script>
+import Apis from '@/js/api.js'
+
+export default {
+  name: 'FileUpload',
+
+  data: function () {
+    return {
+      fileList: [],
+      uploadUrl: process.env.API_ROOT + Apis.FILE.UPLOAD_TEST_CASE_FILE
+    }
+  },
+  props: {
+    countLimit: {
+      type: Number,
+      default: 1
+    },
+    files: Array,
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    files: {
+      immediate: true,
+      handler: function () {
+        if (this.files.length !== this.fileList.length) {
+          this.fileList = []
+          this.files.map(file => {
+            let fileName = file.substring(file.lastIndexOf('/') + 1)
+            fileName = fileName.substring(0, fileName.indexOf('_')) + fileName.substring(fileName.lastIndexOf('.'))
+            this.fileList.push({ name: fileName, url: file })
+          })
+        }
+      }
+    }
+  },
+  methods: {
+    handleExceed (files, fileList) {
+      this.$message.warning(
+        `当前限制选择 ${this.countLimit} 个文件,本次选择了 ${
+          files.length
+        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
+      )
+    },
+    beforeFileUpload (file) {
+      console.log(file)
+      const isPDF = file.type === 'application/pdf'
+      const isDOC = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+      const isEXCEL = file.type === 'application/vnd.ms-excel'
+      const isXLS = file.type === 'application/x-xls'
+      const isTXT = file.type === 'text/plain'
+      const isXLSX = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+      if (!(isDOC || isEXCEL || isPDF || isTXT || isXLS || isXLSX)) {
+        this.$message.error('上传文件只能是 PDF 、 DOC 、DOCX 、XLS、TXT、XLSX 格式!')
+      }
+      return isDOC || isEXCEL || isPDF || isTXT || isXLS || isXLSX
+    },
+    handleUploadSuccess (response, file, fileList) {
+      // 这两个的顺序不能换
+      this.fileList.push(file)
+      this.files.push(response)
+    },
+    fileUploadError (err, file, fileList) { // 文件上传失败调用
+      console.log(err)
+      this.$message.error('上传文件失败!')
+    },
+    handleFileRemove (file) {
+      console.log(this.fileList)
+      for (const i in this.fileList) {
+        if (this.fileList[i].uid === file.uid) {
+          // 这两个的顺序不能换
+          this.fileList.splice(i, 1)
+          this.files.splice(i, 1)
+        }
+      }
+      console.log(this.files)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .upload-file {
+    width: 400px;
+  }
+</style>

+ 103 - 0
src/components/file/ImgUpload.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-upload
+    list-type="picture-card"
+    accept="image/*"
+    :action="uploadUrl"
+    :on-success="handleUploadSuccess"
+    multiple
+    :limit="countLimit"
+    :on-exceed="handleExceed"
+    :before-upload="beforeFileUpload"
+    :on-remove="handleFileRemove"
+    :file-list="fileList"
+    :on-error="imgUploadError"
+    :disabled="disabled"
+  >
+    <i slot="default" class="el-icon-plus"></i>
+  </el-upload>
+</template>
+
+<script>
+import Apis from '@/js/api.js'
+
+export default {
+  name: 'ImgUpload',
+  data: function () {
+    return {
+      fileList: [],
+      uploadUrl: process.env.API_ROOT + Apis.FILE.UPLOAD_TEST_CASE_IMAGE
+    }
+  },
+  props: {
+    countLimit: {
+      type: Number,
+      default: 1
+    },
+    files: Array,
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    files: {
+      immediate: true,
+      handler: function () {
+        if (this.files.length !== this.fileList.length) {
+          this.fileList = []
+          this.files.map(file => {
+            this.fileList.push({ url: file })
+          })
+        }
+      }
+    }
+  },
+  methods: {
+    handleExceed (files, fileList) {
+      this.$message.warning(
+        `当前限制选择 ${this.countLimit} 个文件,本次选择了 ${
+          files.length
+        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
+      )
+    },
+    beforeFileUpload (file) {
+      console.log(file)
+      const isJPG = file.type === 'image/jpeg'
+      const isPNG = file.type === 'image/png'
+      const isLt2M = file.size / 1024 / 1024 < 2
+      if (!isJPG && !isPNG) {
+        this.$message.error('上传头像图片只能是 JPG 和 PNG 格式!')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 2MB!')
+      }
+      return (isJPG || isPNG) && isLt2M
+    },
+    handleUploadSuccess (response, file, fileList) {
+      // 这两个的顺序不能换
+      this.fileList.push(file)
+      this.files.push(response)
+    },
+    // handleAvatarSuccess (res, file) { // 图片上传成功
+    //   console.log(res)
+    //   console.log(file)
+    //   this.imageUrl = URL.createObjectURL(file.raw)
+    // },
+    imgUploadError (err, file, fileList) { // 图片上传失败调用
+      console.log(err)
+      this.$message.error('上传图片失败!')
+    },
+    handleFileRemove (file) {
+      console.log(this.fileList)
+      for (const i in this.fileList) {
+        if (this.fileList[i].uid === file.uid) {
+          // 这两个的顺序不能换
+          this.fileList.splice(i, 1)
+          this.files.splice(i, 1)
+        }
+      }
+      console.log(this.files)
+    }
+  }
+}
+</script>

+ 75 - 0
src/components/project/ProjectSearch.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-select v-model="selectedProjectCode" filterable :filter-method="projectDataFilter" @click.native="eqNoClick">
+    <el-option
+      v-for="project in searchProjects"
+      :key="project.code"
+      :label="project.name"
+      :value="project.code"
+    />
+  </el-select>
+</template>
+
+<script>
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import {mapGetters} from 'vuex'
+
+export default {
+  name: 'ProjectSearch',
+  data: function () {
+    return {
+      projects: [],
+      searchProjects: [],
+      selectedProjectCode: ''
+    }
+  },
+  props: {
+    firstSelectedProjectCode: {
+      type: String,
+      default: ''
+    }
+  },
+  created () {
+    this.selectedProjectCode = this.firstSelectedProjectCode
+    this.getSimpleProjectDatas()
+  },
+  watch: {
+    'selectedProjectCode': {
+      immediate: true,
+      handler: function () {
+        if (this.getRefreshTaskListFunc()) {
+          this.getRefreshTaskListFunc()(this.selectedProjectCode)
+        }
+      }
+    }
+  },
+  methods: {
+    ...mapGetters(['getRefreshTaskListFunc']),
+    projectDataFilter (val) {
+      if (val) {
+        this.searchProjects = this.projects.filter(project => {
+          return project.name.includes(val)
+        })
+      } else {
+        this.searchProjects = this.projects
+      }
+    },
+    eqNoClick () {
+      this.searchProjects = this.projects
+    },
+    getSimpleProjectDatas () {
+      Http.get(Api.PROJECT.GET_SIMPLE_DATAS).then((res) => {
+        this.projects = res.data
+        this.searchProjects = res.data
+      }).catch((error) => {
+        notify('error', '获取项目列表数据失败:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 87 - 0
src/components/project/TaskSearch.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-select v-model="selectedTaskCode" filterable :filter-method="taskDataFilter" @click.native="eqNoClick">
+    <el-option
+      v-for="task in searchTasks"
+      :key="task.code"
+      :label="task.name"
+      :value="task.code"
+    />
+  </el-select>
+</template>
+
+<script>
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import {mapActions} from 'vuex'
+
+export default {
+  name: 'TaskSearch',
+  data: function () {
+    return {
+      tasks: [],
+      searchTasks: [],
+      selectedTaskCode: ''
+    }
+  },
+  props: {
+    firstSelectedTaskCode: {
+      type: String,
+      default: ''
+    },
+    selectedProjectCode: {
+      type: String,
+      default: ''
+    },
+    selectedCallback: {
+      type: Function
+    }
+  },
+  created () {
+    this.selectedTaskCode = this.firstSelectedTaskCode
+    this.setRefreshTaskListFunc(this.getSimpleTaskDatas)
+  },
+  watch: {
+    'selectedTaskCode': {
+      immediate: false,
+      handler: function () {
+        if (this.selectedCallback) {
+          this.selectedCallback(this.selectedTaskCode)
+        }
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['setRefreshTaskListFunc']),
+    taskDataFilter (val) {
+      if (val) {
+        this.searchTasks = this.tasks.filter(task => {
+          return task.name.includes(val)
+        })
+      } else {
+        this.searchTasks = this.tasks
+      }
+    },
+    eqNoClick () {
+      this.searchTasks = this.tasks
+    },
+    getSimpleTaskDatas (projectCode) {
+      Http.get(Api.TASK.GET_SIMPLE_DATAS_BY_PROJECT.replace('{projectCode}', projectCode)).then((res) => {
+        this.tasks = res.data
+        this.searchTasks = res.data
+        this.selectedTaskData = this.searchTasks.find(task => task.code === this.selectedTaskCode)
+        if (!this.selectedTaskData) {
+          this.selectedTaskData = this.tasks[0]
+          this.selectedTaskCode = this.selectedTaskData.code
+        }
+      }).catch((error) => {
+        notify('error', '获取任务列表数据失败:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 38 - 8
src/components/task/Task.vue

@@ -226,10 +226,13 @@
               <el-button v-if="taskOperationControl.taskRecommend" type="primary" size="mini" @click="recommendTask()">任务推荐
               </el-button>
 
-              <el-button v-if="taskOperationControl.uploadReport" type="primary" size="mini" @click="toCreateReport()">
-                上传报告
+              <el-button v-if="taskOperationControl.testCaseManage" type="primary" size="mini" @click="toTestCases()">
+                众测执行
               </el-button>
-              <el-button v-if="taskOperationControl.taskDemonstrate" type="success" size="mini" @click="gotoDataboard()">
+              <el-button v-if="taskOperationControl.testCaseExam" type="primary" size="mini" @click="toTestCasesExam()">
+                众测审核
+              </el-button>
+              <el-button v-if="taskOperationControl.testCaseExam" type="success" size="mini" @click="gotoTaskStatistics()">
                 任务面板
               </el-button>
               <!--<div class="btn btn-small btn-info"-->
@@ -333,7 +336,9 @@ export default {
         uploadReport: false,
         writeReport: false,
         taskDemonstrate: false,
-        taskRecommend: false
+        taskRecommend: false,
+        testCaseExam: false,
+        testCaseManage: false
       },
       crowdReportUrl: '',
       wordCloud:[],
@@ -459,8 +464,11 @@ export default {
   },
   methods: {
     //跳转到任务对应的数据面板
-    gotoDataboard(){
-      window.open(this.task.endPoint.token)
+    gotoTaskStatistics () {
+      this.$router.push({
+        name: 'TaskStatisticsPage',
+        params: {projectCode: this.projectId, taskCode: this.taskId}
+      })
     },
     //根据短链接获取生成databoard
     getTaskDataBoard(){
@@ -673,7 +681,7 @@ export default {
         serverCode: '',
       }
       this.taskOperationControl = res.taskOperationControl;
-      // console.log(this.taskOperationControl,'this.taskOperationControl')
+      console.log(this.taskOperationControl)
       this.acceptedUserList = res.acceptedUserList;
       this.crowdReportUrl = res.crowdTaskVO.writeReportUrl;
       this.handleFormatReport(this.acceptedUserList);
@@ -988,8 +996,30 @@ export default {
     },
     reformDate(date) {
       return getFormalTimeFromDate(date)
+    },
+    // 跳转到测试用例管理页面
+    toTestCases () {
+      this.$router.push({
+        name: 'TestCases',
+        params: {
+          taskCode: this.taskId
+        }
+      })
+    },
+    // 跳转到测试用例审核页面
+    toTestCasesExam () {
+      console.log(this.user.userVO.id)
+      console.log(this.projectId)
+      this.$router.push({
+        name: 'ExamTestCases',
+        params: {
+          projectCode: this.projectId,
+          taskCode: this.taskId,
+          userId: 0
+        }
+      })
     }
-  },
+  }
 }
 //回收站
 //

+ 63 - 0
src/components/text/ExpendText.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <span>{{isExpend ? text : capitalize(text)}}</span>
+    <span @click="expendClick" class="expend" v-if="showExpend">
+      {{isExpend ? '收起' : '展开'}}
+      <i :class="isExpend ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"/>
+    </span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ExpendText',
+  data: function () {
+    return {
+      isExpend: false
+    }
+  },
+  props: {
+    length: {
+      type: Number,
+      default: 100
+    },
+    text: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    showExpend: function () {
+      return this.text && this.text.length > this.length
+    }
+  },
+  watch: {
+    'text': function () {
+      this.isExpend = false
+    }
+  },
+  methods: {
+    capitalize (value) {
+      if (!value) return ''
+      value = value.toString()
+      if (value.length > 100) {
+        return value.substr(0, 100)
+      } else {
+        return value
+      }
+    },
+    expendClick () {
+      this.isExpend = !this.isExpend
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .expend {
+    color: coral
+  }
+  .expend:hover {
+    cursor: pointer;
+  }
+</style>

+ 7 - 3
src/config/index.js

@@ -134,11 +134,15 @@ export const setConfig = (conf) => {
   CONFIG = conf;
 }
 
+//本地开发使用
+export const login_url = (process.env.ENV_CONFIG === 'dev' || process.env.ENV_CONFIG === 'test') ? '#/login' : 'http://user.cofortest.com:8081/page/login?redirect=http%3a%2f%2fwww.cofortest.com%2f%23%2fhome'
+export const register_url = (process.env.ENV_CONFIG === 'dev' || process.env.ENV_CONFIG === 'test') ? '' : 'http://user.cofortest.com:8081/page/register'
+
 // 开发使用
-// export const login_url = 'http://crowd.dev.mooctest.net/page/login?redirect=http%3a%2f%2fcrowd.dev.mooctest.net%2f%23%2fhome'
+// export const login_url = 'http://crowd.dev.mooctest.net:8281/page/login?redirect=http%3a%2f%2fdev.mooctest.net%3A5757%2f%23%2fhome'
 // export const register_url = 'http://crowd.dev.mooctest.net/page/register'
 
 
 // 线上使用
-export const login_url = 'http://user.cofortest.com:8081/page/login?redirect=http%3a%2f%2fwww.cofortest.com%2f%23%2fhome'
-export const  register_url = 'http://user.cofortest.com:8081/page/register'
+// export const login_url = 'http://user.cofortest.com:8081/page/login?redirect=http%3a%2f%2fwww.cofortest.com%2f%23%2fhome'
+// export const  register_url = 'http://user.cofortest.com:8081/page/register'

+ 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

+ 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

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

@@ -0,0 +1,31 @@
+import store from '@/store'
+
+function checkPermission(el, binding) {
+  const { value } = binding
+  const roles = store.getters && store.getters.roles
+
+  if (value && value instanceof Array) {
+    if (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']"`)
+  }
+}
+
+export default {
+  inserted(el, binding) {
+    checkPermission(el, binding)
+  },
+  update(el, binding) {
+    checkPermission(el, binding)
+  }
+}

+ 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]
+  }
+}

+ 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)

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h36.571V128H0V54.857zM91.429 27.43H128V128H91.429V27.429zM45.714 0h36.572v128H45.714V0z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.857 118.857h64V73.143H89.143c-1.902 0-3.52-.668-4.855-2.002-1.335-1.335-2.002-2.954-2.002-4.855V36.57H54.857v82.286zM73.143 16v-4.571a2.2 2.2 0 0 0-.677-1.61 2.198 2.198 0 0 0-1.609-.676H20.571c-.621 0-1.158.225-1.609.676a2.198 2.198 0 0 0-.676 1.61V16a2.2 2.2 0 0 0 .676 1.61c.451.45.988.676 1.61.676h50.285c.622 0 1.158-.226 1.61-.677.45-.45.676-.987.676-1.609zm18.286 48h21.357L91.43 42.642V64zM128 73.143v48c0 1.902-.667 3.52-2.002 4.855-1.335 1.335-2.953 2.002-4.855 2.002H52.57c-1.901 0-3.52-.667-4.854-2.002-1.335-1.335-2.003-2.953-2.003-4.855v-11.429H6.857c-1.902 0-3.52-.667-4.855-2.002C.667 106.377 0 104.759 0 102.857v-96c0-1.902.667-3.52 2.002-4.855C3.337.667 4.955 0 6.857 0h77.714c1.902 0 3.52.667 4.855 2.002 1.335 1.335 2.003 2.953 2.003 4.855V30.29c1 .622 1.856 1.29 2.569 2.003l29.147 29.147c1.335 1.335 2.478 3.145 3.429 5.43.95 2.287 1.426 4.383 1.426 6.291v-.018z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h54.857v54.857H0V0zm0 73.143h54.857V128H0V73.143zm73.143 0H128V128H73.143V73.143zm27.428-18.286C115.72 54.857 128 42.577 128 27.43 128 12.28 115.72 0 100.571 0 85.423 0 73.143 12.28 73.143 27.429c0 15.148 12.28 27.428 27.428 27.428z"/></svg>

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


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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M73.137 29.08h-9.209 29.7L63.886.093 34.373 29.08h20.49v27.035H27.238v17.948h27.625v27.133h18.274V74.063h27.41V56.115h-27.41V29.08zm-9.245 98.827l27.518-26.711H36.59l27.302 26.71zM.042 64.982l27.196 27.029V38.167L.042 64.982zm100.505-26.815V92.01l27.41-27.029-27.41-26.815z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M106.133 67.2a4.797 4.797 0 0 0-4.8 4.8c0 .187.014.36.027.533h-.027V118.4H9.6V26.667h50.133c2.654 0 4.8-2.147 4.8-4.8 0-2.654-2.146-4.8-4.8-4.8H9.6a9.594 9.594 0 0 0-9.6 9.6V118.4c0 5.307 4.293 9.6 9.6 9.6h91.733c5.307 0 9.6-4.293 9.6-9.6V72.533h-.026c.013-.173.026-.346.026-.533 0-2.653-2.146-4.8-4.8-4.8z"/><path d="M125.16 13.373L114.587 2.8c-3.747-3.747-9.854-3.72-13.6.027l-52.96 52.96a4.264 4.264 0 0 0-.907 1.36L33.813 88.533c-.746 1.76-.226 3.534.907 4.68 1.133 1.147 2.92 1.667 4.693.92l31.4-13.293c.507-.213.96-.52 1.36-.907l52.96-52.96c3.747-3.746 3.774-9.853.027-13.6zM66.107 72.4l-18.32 7.76 7.76-18.32L92.72 24.667l10.56 10.56L66.107 72.4zm52.226-52.227l-8.266 8.267-10.56-10.56 8.266-8.267.027-.026 10.56 10.56-.027.026z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M88.883 119.565c-7.284 0-19.434 2.495-21.333 8.25v.127c-4.232.13-5.222 0-7.108 0-1.895-5.76-14.045-8.256-21.333-8.256H0V0h42.523c9.179 0 17.109 5.47 21.47 13.551C68.352 5.475 76.295 0 85.478 0H128v119.57l-39.113-.005h-.004zM60.442 24.763c0-9.651-8.978-16.507-17.777-16.507H7.108V111.43H39.11c7.054-.14 18.177.082 21.333 6.12v-4.628c-.134-5.722-.004-13.522 0-13.832V27.413l.004-2.655-.004.005zm60.442-16.517h-35.55c-8.802 0-17.78 6.856-17.78 16.493v74.259c.004.32.138 8.115 0 13.813v4.627c3.155-6.022 14.279-6.26 21.333-6.114h32V8.25l-.003-.005z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="96" xmlns="http://www.w3.org/2000/svg"><path d="M64.125 56.975L120.188.912A12.476 12.476 0 0 0 115.5 0h-103c-1.588 0-3.113.3-4.513.838l56.138 56.137z"/><path d="M64.125 68.287l-62.3-62.3A12.42 12.42 0 0 0 0 12.5v71C0 90.4 5.6 96 12.5 96h103c6.9 0 12.5-5.6 12.5-12.5v-71a12.47 12.47 0 0 0-1.737-6.35L64.125 68.287z"/></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/excel.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.208 16.576v8.384h38.72v5.376h-38.72v8.704h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.512h38.72v5.376h-38.72v11.136H128v-94.72H78.208zM0 114.368L72.128 128V0L0 13.632v100.736z"/><path d="M28.672 82.56h-11.2l14.784-23.488-14.08-22.592h11.52l8.192 14.976 8.448-14.976h11.136l-14.08 22.208L58.368 82.56H46.656l-8.768-15.68z"/></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>

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

@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

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


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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.482 70.131l36.204 16.18 69.932-65.485-61.38 70.594 46.435 18.735c1.119.425 2.397-.17 2.797-1.363v-.085L127.998.047 1.322 65.874c-1.12.597-1.519 1.959-1.04 3.151.32.511.72.937 1.2 1.107zm44.676 57.821L64.22 107.26l-18.062-7.834v28.527z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 0 1 4.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 0 1 2.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M83.287 103.01c-1.57-3.84-6.778-10.414-15.447-19.548-2.327-2.444-2.182-4.306-1.338-9.862v-.64c.553-3.81 1.513-6.05 14.313-8.087 6.516-1.018 8.203 1.57 10.589 5.178l.785 1.193a12.625 12.625 0 0 0 6.43 5.207c1.134.524 2.53 1.164 4.421 2.24 4.596 2.53 4.596 5.41 4.596 11.753v.727a26.91 26.91 0 0 1-5.178 17.454 59.055 59.055 0 0 1-19.025 11.026c3.49-6.546.814-14.313 0-16.553l-.146-.087zM64 5.12a58.502 58.502 0 0 1 25.484 5.818 54.313 54.313 0 0 0-12.859 10.327c-.93 1.28-1.716 2.473-2.472 3.579-2.444 3.694-3.637 5.352-5.818 5.614a25.105 25.105 0 0 1-4.219 0c-4.276-.29-10.094-.64-11.956 4.422-1.193 3.23-1.396 11.956 2.444 16.495.66 1.077.778 2.4.32 3.578a7.01 7.01 0 0 1-2.066 3.229 18.938 18.938 0 0 1-2.909-2.91 18.91 18.91 0 0 0-8.32-6.603c-1.25-.349-2.647-.64-3.985-.93-3.782-.786-8.03-1.688-9.019-3.812a14.895 14.895 0 0 1-.727-5.818 21.935 21.935 0 0 0-1.396-9.25 8.873 8.873 0 0 0-5.557-4.946A58.705 58.705 0 0 1 64 5.12zM0 64c0 35.346 28.654 64 64 64 35.346 0 64-28.654 64-64 0-35.346-28.654-64-64-64C28.654 0 0 28.654 0 64z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.585 12.087c0 6.616 3.974 11.98 8.877 11.98 4.902 0 8.877-5.364 8.877-11.98 0-6.616-3.975-11.98-8.877-11.98-4.903 0-8.877 5.364-8.877 11.98zM125.86.107H35.613c-1.268 0-2.114 1.426-2.114 2.852v18.255c0 1.712 1.057 2.853 2.114 2.853h90.247c1.268 0 2.114-1.426 2.114-2.853V2.96c0-1.711-1.057-2.852-2.114-2.852zM.106 62.86c0 6.615 3.974 11.979 8.876 11.979 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zM124.17 50.88H33.921c-1.268 0-2.114 1.425-2.114 2.851v18.256c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852V53.73c0-1.426-.846-2.852-2.114-2.852zM.106 115.913c0 6.616 3.974 11.98 8.876 11.98 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zm124.064-11.98H33.921c-1.268 0-2.114 1.426-2.114 2.853v18.255c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852v-18.255c0-1.427-.846-2.853-2.114-2.853z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M119.88 49.674h-7.987V39.52C111.893 17.738 90.45.08 63.996.08 37.543.08 16.1 17.738 16.1 39.52v10.154H8.113c-4.408 0-7.987 2.94-7.987 6.577v65.13c0 3.637 3.57 6.577 7.987 6.577H119.88c4.407 0 7.987-2.94 7.987-6.577v-65.13c-.008-3.636-3.58-6.577-7.987-6.577zm-23.953 0H32.065V39.52c0-14.524 14.301-26.295 31.931-26.295 17.63 0 31.932 11.777 31.932 26.295v10.153z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

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

@@ -0,0 +1 @@
+<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M869.073 277.307H657.111V65.344l211.962 211.963zm-238.232 26.27V65.344l-476.498-.054v416.957h714.73v-178.67H630.841zm-335.836 360.57c-5.07-3.064-10.944-5.133-17.61-6.201-6.67-1.064-13.603-1.6-20.81-1.6h-48.821v85.641h48.822c7.206 0 14.14-.532 20.81-1.6 6.665-1.065 12.54-3.133 17.609-6.202 5.064-3.063 9.134-7.406 12.208-13.007 3.065-5.602 4.6-12.937 4.6-22.011 0-9.07-1.535-16.408-4.6-22.01-3.074-5.603-7.144-9.94-12.208-13.01zM35.82 541.805v416.904h952.358V541.805H35.821zm331.421 191.179c-3.6 11.071-9.343 20.879-17.209 29.413-7.874 8.542-18.078 15.408-30.617 20.61-12.544 5.206-27.747 7.807-45.621 7.807h-66.036v102.45h-62.831V607.517h128.867c17.874 0 33.077 2.6 45.62 7.802 12.541 5.207 22.745 12.076 30.618 20.615 7.866 8.538 13.604 18.277 17.21 29.212 3.6 10.943 5.401 22.278 5.401 34.018 0 11.477-1.8 22.752-5.402 33.819zM644.9 806.417c-5.343 17.61-13.408 32.818-24.212 45.627-10.807 12.803-24.283 22.879-40.423 30.213-16.146 7.343-35.155 11.007-57.03 11.007h-123.26V607.518h123.26c18.41 0 35.552 2.941 51.428 8.808 15.873 5.869 29.618 14.671 41.22 26.412 11.608 11.744 20.674 26.411 27.217 44.02 6.535 17.61 9.803 38.288 9.803 62.035 0 20.81-2.67 40.02-8.003 57.624zm245.362-146.07h-138.07v66.03h119.66v48.829h-119.66v118.058h-62.83V607.518h200.9v52.829h-.001zm-318.2 25.611c-6.402-8.266-14.877-14.604-25.412-19.01-10.544-4.402-23.551-6.602-39.019-6.602h-44.825v180.088h56.029c9.07 0 17.872-1.463 26.415-4.401 8.535-2.932 16.14-7.802 22.812-14.609 6.665-6.8 12.007-15.667 16.007-26.61 4.003-10.94 6.003-24.275 6.003-40.021 0-14.408-1.4-27.416-4.202-39.019-2.8-11.607-7.406-21.542-13.808-29.816zm0 0"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M104.185 95.254c8.161 7.574 13.145 17.441 13.145 28.28 0 1.508-.098 2.998-.285 4.466h-10.784c.238-1.465.403-2.948.403-4.465 0-8.983-4.36-17.115-11.419-23.216C86 104.66 75.355 107.162 64 107.162c-11.344 0-21.98-2.495-31.22-6.83-7.064 6.099-11.444 14.218-11.444 23.203 0 1.517.165 3 .403 4.465H10.955a35.444 35.444 0 0 1-.285-4.465c0-10.838 4.974-20.713 13.127-28.291C9.294 85.42.003 70.417.003 53.58.003 23.99 28.656.001 64 .001s63.997 23.988 63.997 53.58c0 16.842-9.299 31.85-23.812 41.673zM64 36.867c-29.454 0-53.33-10.077-53.33 15.342 0 25.418 23.876 46.023 53.33 46.023 29.454 0 53.33-20.605 53.33-46.023 0-25.419-23.876-15.342-53.33-15.342zm24.888 25.644c-3.927 0-7.111-2.665-7.111-5.953 0-3.288 3.184-5.954 7.11-5.954 3.928 0 7.111 2.666 7.111 5.954s-3.183 5.953-7.11 5.953zm-3.556 16.372c0 4.11-9.55 7.442-21.332 7.442-11.781 0-21.332-3.332-21.332-7.442 0-1.06.656-2.064 1.8-2.976 3.295 2.626 10.79 4.465 19.532 4.465 8.743 0 16.237-1.84 19.531-4.465 1.145.912 1.801 1.916 1.801 2.976zm-46.22-16.372c-3.927 0-7.11-2.665-7.11-5.953 0-3.288 3.183-5.954 7.11-5.954 3.927 0 7.111 2.666 7.111 5.954s-3.184 5.953-7.11 5.953z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M95.648 118.762c0 5.035-3.563 9.121-7.979 9.121H7.98c-4.416 0-7.979-4.086-7.979-9.121C0 100.519 15.408 83.47 31.152 76.75c-9.099-6.43-15.216-17.863-15.216-30.987v-9.128c0-20.16 14.293-36.518 31.893-36.518s31.894 16.358 31.894 36.518v9.122c0 13.137-6.123 24.556-15.216 30.993 15.738 6.726 31.141 23.769 31.141 42.012z"/><path d="M106.032 118.252h15.867c3.376 0 6.101-3.125 6.101-6.972 0-13.957-11.787-26.984-23.819-32.123 6.955-4.919 11.638-13.66 11.638-23.704v-6.985c0-15.416-10.928-27.926-24.39-27.926-1.674 0-3.306.193-4.89.561 1.936 4.713 3.018 9.974 3.018 15.526v9.121c0 13.137-3.056 23.111-11.066 30.993 14.842 4.41 27.312 23.42 27.541 41.509z"/></svg>

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


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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg>

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


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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M31.652 93.206h33.401c1.44 2.418 3.077 4.663 4.93 6.692h-38.33v-6.692zm0-10.586h28.914a44.8 44.8 0 0 1-1.264-6.688h-27.65v6.688zm0-17.27H59.39c.288-2.286.714-4.532 1.34-6.687H31.65v6.687h.003zm53.913 44.84v5.85c0 2.798-2.095 5.075-4.667 5.075h-70.07c-2.576 0-4.663-2.277-4.663-5.075V31.26l23.22-20.96v22.25H17.16v6.688h18.39V6.688h45.348c2.576 0 4.667 2.277 4.667 5.066v20.009c1.987-.675 4.053-1.128 6.17-1.445v-18.56C91.738 5.28 86.874 0 80.902 0H31.15L0 28.118v87.917c0 6.48 4.859 11.759 10.832 11.759h70.07c5.974 0 10.837-5.27 10.837-11.759v-4.41c-2.117-.312-4.183-.765-6.17-1.435h-.004zM23.279 58.667h-7.96v6.688h7.96v-6.688zm-7.956 41.23h7.96v-6.691h-7.96v6.692zm7.956-23.96h-7.96v6.687h7.96v-6.688zm89.718-15.042l-4.896-4.07-12.447 17.613-11.19-9.305-3.762 5.311 16.091 13.38 16.204-22.929zM128 70.978c0-18.632-13.97-33.782-31.147-33.782-17.168 0-31.135 15.155-31.135 33.782 0 18.628 13.97 33.783 31.135 33.783 17.172 0 31.143-15.15 31.143-33.783H128zm-6.17 0c0 14.933-11.203 27.1-24.981 27.1-13.77 0-24.987-12.158-24.987-27.1 0-14.941 11.195-27.099 24.987-27.099 13.778 0 24.982 12.158 24.982 27.1z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M70.66 4.328l14.01 29.693c1.088 2.29 3.177 3.882 5.603 4.25l31.347 4.76c6.087.926 8.528 8.756 4.117 13.247L103.05 79.395c-1.75 1.78-2.544 4.352-2.132 6.867l5.352 32.641c1.043 6.337-5.33 11.182-10.778 8.19l-28.039-15.409a7.13 7.13 0 0 0-6.91 0l-28.039 15.41c-5.448 2.99-11.821-1.854-10.777-8.19l5.352-32.642c.415-2.515-.387-5.088-2.136-6.867L2.264 56.278C-2.146 51.787.286 43.957 6.38 43.031l31.343-4.76c2.419-.368 4.51-1.96 5.595-4.25L57.334 4.328c2.728-5.77 10.605-5.77 13.325 0z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.921.052H49.08c-1.865 0-3.198 1.599-3.198 3.464v6.661c0 1.865 1.6 3.464 3.198 3.464h29.84c1.865 0 3.198-1.599 3.198-3.464V3.516C82.385 1.65 80.786.052 78.92.052zm45.563 0H94.642c-1.865 0-3.464 1.599-3.464 3.464v6.661c0 1.865 1.599 3.464 3.464 3.464h29.842c1.865-.266 3.464-1.599 3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464zm0 22.382H40.02c-1.866 0-3.464-1.599-3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464H3.516C1.65.052.052 1.651.052 3.516V124.75c0 1.598 1.599 3.197 3.464 3.197h120.968c1.865 0 3.464-1.599 3.464-3.464V25.898c0-1.865-1.599-3.464-3.464-3.464z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M125.5 36.984L95.336 2.83C93.735 1.018 91.565 0 89.3 0c-2.263 0-4.433 1.018-6.033 2.83l-3.786 4.286c-1.6 1.812-3.77 2.83-6.032 2.831H54.553c-2.263 0-4.434-1.018-6.033-2.83L44.734 2.83C43.134 1.018 40.964 0 38.701 0c-2.263 0-4.434 1.018-6.034 2.83L2.5 36.984C.9 38.796 0 41.254 0 43.815c0 2.562.899 5.02 2.5 6.831L14.565 64.31c2.178 2.468 5.367 3.403 8.33 2.444 1.35-.435 2.709.592 2.709 2.18v49.407c0 5.313 3.84 9.66 8.532 9.66h59.726c4.693 0 8.532-4.347 8.532-9.66V68.934c0-1.59 1.36-2.616 2.71-2.181 2.962.96 6.15.024 8.329-2.444L125.5 50.646c1.6-1.811 2.499-4.269 2.499-6.83 0-2.563-.899-5.02-2.5-6.832z"/></svg>

+ 1 - 0
src/icons/svg/tree-table.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M44.8 0h79.543C126.78 0 128 1.422 128 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H44.8c-2.438 0-3.657-1.422-3.657-4.267V4.267C41.143 1.422 42.362 0 44.8 0zm22.857 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 80 64 78.578 64 75.733V52.267C64 49.422 65.219 48 67.657 48zm0 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 128 64 126.578 64 123.733v-23.466C64 97.422 65.219 96 67.657 96zM50.286 68.267c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V32h6.4c2.02 0 3.658-1.91 3.658-4.267V4.267C27.429 1.91 25.79 0 23.77 0H3.657C1.637 0 0 1.91 0 4.267v23.466C0 30.09 1.637 32 3.657 32h6.4v80c0 2.356 1.638 4.267 3.657 4.267h36.572c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V68.267h32.915z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="110" xmlns="http://www.w3.org/2000/svg"><path d="M86.635 33.334c1.467 0 2.917.113 4.358.283C87.078 14.392 67.58.111 45.321.111 20.44.111.055 17.987.055 40.687c0 13.104 6.781 23.863 18.115 32.209l-4.527 14.352 15.82-8.364c5.666 1.182 10.207 2.395 15.858 2.395 1.42 0 2.829-.073 4.227-.189-.886-3.19-1.398-6.53-1.398-9.996 0-20.845 16.98-37.76 38.485-37.76zm-24.34-12.936c3.407 0 5.665 2.363 5.665 5.954 0 3.576-2.258 5.97-5.666 5.97-3.392 0-6.795-2.395-6.795-5.97 0-3.591 3.403-5.954 6.795-5.954zM30.616 32.323c-3.393 0-6.818-2.395-6.818-5.971 0-3.591 3.425-5.954 6.818-5.954 3.392 0 5.65 2.363 5.65 5.954 0 3.576-2.258 5.97-5.65 5.97z"/><path d="M127.945 70.52c0-19.075-18.108-34.623-38.448-34.623-21.537 0-38.5 15.548-38.5 34.623 0 19.108 16.963 34.622 38.5 34.622 4.508 0 9.058-1.2 13.584-2.395l12.414 7.167-3.404-11.923c9.087-7.184 15.854-16.712 15.854-27.471zm-50.928-5.97c-2.254 0-4.53-2.362-4.53-4.773 0-2.378 2.276-4.771 4.53-4.771 3.422 0 5.665 2.393 5.665 4.771 0 2.41-2.243 4.773-5.665 4.773zm24.897 0c-2.24 0-4.498-2.362-4.498-4.773 0-2.378 2.258-4.771 4.498-4.771 3.392 0 5.665 2.393 5.665 4.771 0 2.41-2.273 4.773-5.665 4.773z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.527 116.793c.178.008.348.024.527.024h40.233c4.711-.005 8.53-3.677 8.534-8.21V18.895c-.004-4.532-3.823-8.204-8.534-8.209H79.054c-.179 0-.353.016-.527.024V0L0 10.082v107.406l78.527 10.342v-11.037zm0-101.362c.174-.024.348-.052.527-.052h40.233c2.018 0 3.659 1.578 3.659 3.52v89.713c-.003 1.942-1.64 3.517-3.659 3.519H79.054c-.179 0-.353-.028-.527-.052V15.431zM30.262 75.757l-18.721-.46V72.37l11.3-16.673v-.148l-10.266.164v-4.51l17.504-.44v3.264L18.696 70.76v.144l11.566.176v4.678zm9.419.231l-5.823-.144V50.671l5.823-.144v25.461zm22.255-11.632c-2.168 1.922-5.353 2.76-9.02 2.736-.702.004-1.402-.04-2.097-.131v9.303l-5.997-.148V50.743c1.852-.352 4.473-.647 8.218-.743 3.838-.096 6.608.539 8.48 1.913 1.807 1.306 3.032 3.5 3.032 6.112s-.926 4.833-2.612 6.331h-.004zM53.36 54.45c-.856-.01-1.71.083-2.541.275v7.682c.523.116 1.167.152 2.06.152 3.301-.004 5.36-1.614 5.36-4.314 0-2.425-1.772-3.843-4.875-3.791l-.004-.004zm39.847-37.066h9.564v3.795h-9.564v-3.795zm-9.568 5.68h9.564v3.8h-9.564v-3.8zm9.568 6.216h9.564v3.799h-9.564V29.28zm0 12h9.564v3.794h-9.564V41.28zm-9.568-6.096h9.564v3.795h-9.564v-3.795zm9.472 47.064c2.512 0 4.921-.96 6.697-2.67 1.776-1.708 2.773-4.026 2.772-6.442l-1.748-15.263c0-5.033-2.492-9.112-7.725-9.112-5.232 0-7.72 4.079-7.72 9.112l-1.752 15.263c-.001 2.417.996 4.735 2.773 6.444 1.777 1.71 4.187 2.669 6.7 2.668h.003zm-3.135-16.75h6.27v12.743h-6.27V65.5z"/></svg>

+ 22 - 0
src/icons/svgo.yml

@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'

+ 62 - 24
src/js/api.js

@@ -13,6 +13,7 @@ export default {
     END_PROJECT: '/api/project/{projectId}/status/finished',
     MORE_HOT_PROJECT: '/api/square/hotProject/list',
     CROWD_PROJECT: '/api/common/index/crowd/project/{code}',
+    GET_SIMPLE_DATAS: '/api/simpleprojectdatas',
   },
   TASK: {
     GET_TASK: '/api/project/{projectId}/task/{taskId}/',
@@ -24,8 +25,14 @@ export default {
     SUBMIT_TASK: '/api/project/{projectId}/task/{taskId}/status/commit',
     END_TASK: '/api/project/{projectId}/task/{taskId}/status/finished',
     MORE_HOT_TASK: '/api/square/hotTasks/list',
-    GET_TASK_CLOUD:'/api/project/{projectId}/task/{taskId}/word',
-    GET_TOKEN:'/api/project/{projectCode}/task/{taskCode}/token'
+    GET_TASK_CLOUD: '/api/project/{projectId}/task/{taskId}/word',
+    GET_TOKEN: '/api/project/{projectCode}/task/{taskCode}/token',
+    GET_SIMPLE_USER_TASK_DATAS: '/api/simpleusertaskdatas',
+    GET_SIMPLE_DATAS_BY_PROJECT: '/api/simpletaskdatas/{projectCode}',
+    GET_TASK_USER_DATAS: '/api/project/{projectCode}/task/{taskCode}/taskusers',
+    COMMIT_TASK: '/api/task/{taskCode}/commit',
+    TASK_STATISTICS: '/api/task/{taskCode}/statistics',
+    TASK_COMPLETION: '/api/task/{taskCode}/completion'
   },
   REPORT: {
     GET_TASK_REPORT: '/api/project/{projectId}/task/{taskId}/report/{reportId}/',
@@ -38,11 +45,13 @@ export default {
     DELETE_PROJECT_REPORT: ''
   },
   FILE: {
-    REQUIREMENT_FILE: '/api/files/requirementfile/{userId}/',
-    APK: '/api/files/apk/{userId}/',
-    UPLOAD_REPORT_FILE: '/api/files/report/{userId}/',
+    REQUIREMENT_FILE: '/api/files/requirementfile/0/',
+    APK: '/api/files/apk/0/',
+    UPLOAD_REPORT_FILE: '/api/files/report/0/',
     UPLOAD_EXCEL: '',
-    UPLOAD_IMAGE: '/api/files/image/{userId}/',
+    UPLOAD_IMAGE: '/api/files/image/0/',
+    UPLOAD_TEST_CASE_IMAGE: '/api/files/testcaseimage/',
+    UPLOAD_TEST_CASE_FILE: '/api/files/testcasefile/',
     GET_TEMPLATE_EXCEL_FILE: ''
   },
   USER: {
@@ -57,23 +66,23 @@ export default {
     UPDATE_INDIVIDUAL_AUTHENTICATION_INFO: '/api/user/{userId}/personalAuth',
     UPDATE_ENTERPRISE_AUTHENTICATION_INFO: '/api/user/{userId}/enterpriseAuth',
     UPDATE_AGENCY_AUTHENTICATION_INFO: '/api/user/{userId}/agency/',
-    UPDATE_AGENCY_RESOURCE_AND_ABILITY:'/api/user/{userId}/agency/resource',
+    UPDATE_AGENCY_RESOURCE_AND_ABILITY: '/api/user/{userId}/agency/resource',
     GET_INDIVIDUAL_AUTHENTICATION_INFO: '/api/user/{userId}/personalAuth',
     GET_ENTERPRISE_AUTHENTICATION_INFO: '/api/user/{userId}/enterpriseAuth',
     GET_AGENCY_AUTHENTICATION_INFO: '/api/user/{userId}/agency',
     GET_AGENCY_AUTHENTICATION_INFO_COMMON: '/api/user/{userId}/agency/common',
     GET_ALL_HANDLING_AUTH_INFO: '/api/user/authentication/handling',
     GET_ALL_HANDLED_AUTH_INFO: '/api/user/authentication/handled',
-    PASS_AGENCY_AUTH:'/api/user/{userId}/agency/status/accept',
-    PASS_ENTERPRISE_AUTH:'/api/user/{userId}/enterpriseAuth/status/accept',
-    PASS_INDIVIDUAL_AUTH:'/api/user/{userId}/personalAuth/status/accept',
-    REJECT_AGENCY_AUTH:'/api/user/{userId}/agency/status/reject',
-    REJECT_ENTERPRISE_AUTH:'/api/user/{userId}/enterpriseAuth/status/reject',
-    REJECT_INDIVIDUAL_AUTH:'/api/user/{userId}/personalAuth/status/reject',
-    GET_DETAIL: "/api/user/detail/{userId}",
-    GET_ADDRESS: "/api/index/address",
-    IS_PART: "/api/common/check/create/project/{userId}",
-    IS_AGENCY: "/api/common/check/accept/task/{userId}",
+    PASS_AGENCY_AUTH: '/api/user/{userId}/agency/status/accept',
+    PASS_ENTERPRISE_AUTH: '/api/user/{userId}/enterpriseAuth/status/accept',
+    PASS_INDIVIDUAL_AUTH: '/api/user/{userId}/personalAuth/status/accept',
+    REJECT_AGENCY_AUTH: '/api/user/{userId}/agency/status/reject',
+    REJECT_ENTERPRISE_AUTH: '/api/user/{userId}/enterpriseAuth/status/reject',
+    REJECT_INDIVIDUAL_AUTH: '/api/user/{userId}/personalAuth/status/reject',
+    GET_DETAIL: '/api/user/detail/{userId}',
+    GET_ADDRESS: '/api/index/address',
+    IS_PART: '/api/common/check/create/project/{userId}',
+    IS_AGENCY: '/api/common/check/accept/task/{userId}'
   },
   PAGE: {
     HOME_PAGE: '/api/common/index/',
@@ -81,25 +90,54 @@ export default {
     MY_CROWD_TEST_PAGE: '/api/common/mycrowd/{userId}',
     TASK_DETAIL_PAGE: '/api/page/taskDetail/{taskId}/',
     PROJECT_DETAIL_PAGE: '/api/project/{projectId}/',
-    REPORT_DETAIL_PAGE: '/api/page/reportDetail/{reportId}/',
+    REPORT_DETAIL_PAGE: '/api/page/reportDetail/{reportId}/'
   },
   AGENCY: {
-    GET_DETAIL: '/api/agency/{agencyId}',
+    GET_DETAIL: '/api/agency/{agencyId}'
   },
   RESOURCE: {
-    GET_DETAIL: '/api/common/index/resource/{code}',
+    GET_DETAIL: '/api/common/index/resource/{code}'
   },
   EXPERT: {
-    GET_DETAIL: '/api/common/index/expert/{id}',
+    GET_DETAIL: '/api/common/index/expert/{id}'
   },
   TECHNOLOGY: {
-    GET_MORE: '/api/technical/more',
+    GET_MORE: '/api/technical/more'
   },
   GENERAL: {
     GET_ALL_INSTITUTIONS: '/api/regionalManager',
     GET_ALL_AGENCIES: '/api/agency/list',
     GET_ALL_ApplicationType: '/api/list/application',
     GET_ALL_TestType: '/api/list/type',
-    GET_ALL_Filed: '/api/list/filed',
-  }
+    GET_ALL_Filed: '/api/list/filed'
+  },
+  TESTCASE: {
+    ADD: '/api/testcase/',
+    UPDATE: '/api/testcase/{id}/',
+    USER_TEST_CASES: '/api/testcase/designer/{taskCode}/{designerId}/{pageNo}/{pageSize}/',
+    DELETE: '/api/testcase/{id}/',
+    USER_DEFECTS: '/api/testcase/designer/defects/{taskCode}/{committerId}/{pageNo}/{pageSize}/',
+    ADD_DEFECT: '/api/testcase/defect/',
+    UPDATE_DEFECT: '/api/testcase/defect/{id}/',
+    DELETE_DEFECT: '/api/testcase/defect/{id}/',
+    EXAM: '/api/testcase/exam/{id}/',
+    UPLOAD_TEST_CASES_FILE: '/api/testcase/upload/{taskCode}/',
+    UPLOAD_DEFECTS_FILE: '/api/testcase/uploaddefects/{taskCode}/'
+  },
+  TESTENV: {
+    ADD: '/api/testenv/',
+    UPDATE: '/api/testenv/{id}/',
+    DELETE: '/api/testenv/{id}/',
+    LIST: '/api/testenv/designer/{taskCode}/{designerId}/'
+  },
+  TESTTOOL: {
+    ADD: '/api/testtool/',
+    UPDATE: '/api/testtool/{id}/',
+    DELETE: '/api/testtool/{id}/',
+    LIST: '/api/testtool/designer/{taskCode}/{designerId}/'
+  },
+  TESTER: {
+    TESTER_TASK_INFO: '/api/tester/task/{userId}/{taskCode}/'
+  },
+  LOGIN: '/api/login2/'
 }

+ 5 - 0
src/js/file.js

@@ -0,0 +1,5 @@
+export function toFileName (url) {
+  let fileName = url.substring(url.lastIndexOf('/') + 1)
+  fileName = fileName.substring(0, fileName.indexOf('_')) + fileName.substring(fileName.lastIndexOf('.'))
+  return fileName
+}

+ 4 - 7
src/js/http.js

@@ -67,11 +67,9 @@ export default {
         .then(response => {
           resolve(response.data)
         }).catch(error => {
-        reject(error.response)
-      })
-
+          reject(error.response)
+        })
     })
-
   },
   put (url, data) {
     return new Promise((resolve, reject) => {
@@ -90,7 +88,7 @@ export default {
     return new Promise((resolve, reject) => {
       axios.delete(handleUrl(url), {data: handleParams(data)}).then(
         (result) => {
-          resolve(result)
+          resolve(result.data)
         }
       ).catch(
         (error) => {
@@ -111,6 +109,5 @@ export default {
         }
       )
     })
-  },
+  }
 }
-

+ 5 - 1
src/main.js

@@ -3,6 +3,7 @@
 import Vue from 'vue'
 import App from './App'
 import router from './router'
+import './icons' // icon
 import 'font-awesome/css/font-awesome.css'
 import './style/main.scss'
 import {getAuthUrls, getCurrentUser, getRolesPermissions, storageGet, storageSave} from '@/js/index'
@@ -10,7 +11,7 @@ import {notify} from '@/constants/index'
 import store from './store'
 import moment from 'moment'
 import vRegion from 'v-region'
-import echarts from "echarts";
+import echarts from 'echarts';
 import Http from '@/js/http.js'
 import {configToJson} from './utils/filters'
 import {
@@ -71,6 +72,9 @@ import {
   Divider
 } from 'element-ui'
 import {setConfig} from "./config";
+import china from 'echarts/map/json/china.json'
+
+echarts.registerMap('china', china)
 Vue.prototype.$moment = moment
 Vue.prototype.$echarts = echarts;
 function getCurrentUserSuccess(res){

+ 161 - 0
src/pages/Statistics/TaskStatistics.vue

@@ -0,0 +1,161 @@
+<template>
+  <div class="dashboard-editor-container" v-if="getTaskData">
+<!--    <div class="board-title">-->
+<!--      {{ getTaskData && getTaskData.taskName }}-->
+<!--&lt;!&ndash;      <el-button type="primary" @click="goToReview" class="review-btn">去评审</el-button>&ndash;&gt;-->
+<!--    </div>-->
+    <project-search :firstSelectedProjectCode="selectedProjectCode"></project-search>
+    <task-search :firstSelectedTaskCode="selectedTaskCode" :selectedCallback="getTaskData"></task-search>
+
+    <panel-group :info="taskInfo" :task-code="selectedTaskCode"/>
+
+    <el-row :gutter="20" style="background:transparent;margin-bottom:32px;height: 650px">
+      <el-col :xs="24" :sm="24" :lg="5" style="height: 100%">
+        <info-card style="height:100%;margin-bottom: 20px" :task="taskInfo"/>
+<!--        <box-card style="height: 37%;position: relative;bottom: 0"/>-->
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="14">
+        <StaffDistribution
+          :task-info="taskInfo"
+          v-if="taskInfo"
+          class="card-shadow">
+        </StaffDistribution>
+        <div style="height: 650px;width: 100%"
+             v-else
+             v-loading="loading"
+             element-loading-text="数据加载中">
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="5" style="height: 100%">
+        <transaction-table :list="taskInfo.testers" :task-code="selectedTaskCode" style="height: 100%" class="card-shadow"/>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="15">
+      <el-col :xs="24" :sm="24" :lg="8">
+        <div class="chart-wrapper card-shadow">
+          <bug-level-chart :counts="taskInfo.defectSeriCounts"/>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="8">
+        <div class="chart-wrapper card-shadow">
+          <bug-time-count-chart :bug-time-counts="taskInfo.defectTimeCountMap"/>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="8">
+        <div class="chart-wrapper card-shadow">
+          <bug-type-chart :gradeList="gradeList" :bug-type-counts="taskInfo.defectTypeCountMap"/>
+        </div>
+      </el-col>
+    </el-row>
+
+  </div>
+</template>
+
+<script>
+import PanelGroup from './components/PanelGroup'
+import BugLevelChart from './components/BugLevelChart'
+import BugTimeCountChart from './components/BugTimeCountChart'
+import BugTypeChart from './components/BugTypeChart'
+import TransactionTable from './components/TransactionTable'
+import TodoList from './components/TodoList'
+import InfoCard from './components/InfoCard'
+import StaffDistribution from './components/StaffDistributionMap'
+import ProjectSearch from '@/components/project/ProjectSearch'
+import TaskSearch from '@/components/project/TaskSearch'
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import Project from '../../components/project/Project'
+
+export default {
+  name: 'TaskBoard',
+  components: {
+    Project,
+    PanelGroup,
+    BugTimeCountChart,
+    BugLevelChart,
+    BugTypeChart,
+    TransactionTable,
+    TodoList,
+    InfoCard,
+    StaffDistribution,
+    ProjectSearch,
+    TaskSearch
+  },
+  data () {
+    return {
+      topData: {},
+      taskInfo: {},
+      workList: [],
+      gradeList: {},
+      workerDistribute: [],
+      loading: true,
+      progress: 0,
+      selectedProjectCode: this.$route.params.projectCode,
+      selectedTaskCode: this.$route.params.taskCode,
+    }
+  },
+  methods: {
+    getTaskData (taskCode) {
+      Http.get(Api.TASK.TASK_STATISTICS.replace('{taskCode}', taskCode)).then((res) => {
+        this.taskInfo = res.data
+      }).catch((error) => {
+        notify('error', '获取任务信息失败:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dashboard-editor-container {
+  padding: 20px;
+  background-color: rgb(240, 242, 245);
+  position: relative;
+
+  .board-title {
+    text-align: center;
+    font-size: 30px;
+    font-weight: bolder;
+    margin-bottom: 15px;
+    position: relative;
+
+    .review-btn {
+      position: absolute;
+      right: 0;
+    }
+  }
+
+  .github-corner {
+    position: absolute;
+    top: 0px;
+    border: 0;
+    right: 0;
+  }
+
+  .chart-wrapper {
+    background: #fff;
+    padding: 16px 16px 0;
+    margin-bottom: 32px;
+  }
+}
+
+.card-shadow {
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+@media (max-width: 1024px) {
+  .chart-wrapper {
+    padding: 8px;
+  }
+}
+
+@media (max-width: 550px) {
+  .review-btn {
+    display: none;
+  }
+
+}
+
+</style>

+ 113 - 0
src/pages/Statistics/components/BugLevelChart.vue

@@ -0,0 +1,113 @@
+<template>
+  <div>
+    <div class="chart-title">bug严重程度分布直方图</div>
+    <div id="bugLevelChart" :class="className" :style="{height:height,width:width}" />
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+const animationDuration = 6000
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    },
+    counts: {
+      type: Array,
+      default: function () {
+        return [0, 0, 0, 0 ,0]
+      }
+    }
+  },
+  watch: {
+    counts: {
+      handler (nv, ov) {
+        this.YValue = this.counts
+        this.initChart()
+      },
+      deep: true
+    }
+  },
+  data () {
+    return {
+      chart: null,
+      XData: ['致命', '严重', '一般', '轻微', '建议'],
+      YValue: [0, 0, 0, 0, 0]
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods:{
+    initChart() {
+      this.chart = echarts.init(document.getElementById('bugLevelChart'), 'macarons')
+      this.chart.setOption({
+        // title:{
+        //   text:'众测成绩分布直方图',
+        //   padding:20
+        // },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          top: 10,
+          left: '2%',
+          right: '2%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: [{
+          name: '严重程度',
+          type: 'category',
+          data: Array.from(this.XData),
+          axisTick: {
+            alignWithLabel: true
+          },
+          axisLabel: {interval: 0}
+        }],
+        yAxis: [{
+          name: '数量',
+          type: 'value',
+          axisTick: {
+            show: false
+          }
+        }],
+        series: [{
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: Array.from(this.YValue),
+          animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>

+ 105 - 0
src/pages/Statistics/components/BugTimeCountChart.vue

@@ -0,0 +1,105 @@
+<template>
+  <div>
+    <div class="chart-title">分时bug提交数量散点图</div>
+    <div id='bugTimeCountChart' :class="className" :style="{height:height,width:width}" />
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts'
+
+require('echarts/theme/macarons') // echarts theme
+// import { getBugSubmitInfo } from  '@/api/databoard'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    },
+    autoResize: {
+      type: Boolean,
+      default: true
+    },
+    bugTimeCounts: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  data () {
+    return {
+      datas: []
+    }
+  },
+  watch: {
+    bugTimeCounts: {
+      handler (nv, ov) {
+        this.datas = []
+        const hours = Object.keys(nv)
+        hours.forEach(hour => this.datas.push([parseInt(hour), nv[hour]]))
+        console.log(this.datas)
+        this.initChart()
+      }
+    }
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart () {
+      this.chart = echarts.init(document.getElementById('bugTimeCountChart'), 'macarons')
+      this.setOptions()
+    },
+    setOptions () {
+      this.chart.setOption({
+        tooltip: {
+          // trigger: 'axis',
+          showDelay: 0,
+          formatter: function (params) {
+            return '任务创建之后的第' + params.value[0] +
+                '小时提交了' + params.value[1] +
+                '个bug'
+          }
+        },
+        xAxis: {
+          // name: '任务开始之后的小时数',
+          type: 'value',
+          scale: true,
+          axisLabel: {
+            formatter: '{value}时'
+          },
+          splitLine: {
+            show: false
+          }
+        },
+        yAxis: {
+          name: 'bug数量(个)'
+        },
+        series: [
+          {
+            symbolSize: 20,
+            data: this.datas,
+            type: 'scatter'
+          }
+        ]
+      })
+    }
+  }
+}
+</script>

+ 99 - 0
src/pages/Statistics/components/BugTypeChart.vue

@@ -0,0 +1,99 @@
+<template>
+  <div>
+    <div class="chart-title">缺陷类型分布饼状图</div>
+    <div id='bugTypeCharts' :class="className" :style="{height:height,width:width}"/>
+  </div>
+</template>
+
+<script>
+  import echarts from 'echarts'
+  require('echarts/theme/macarons') // echarts theme
+  import resize from './mixins/resize'
+
+  export default {
+    mixins: [resize],
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '100%'
+      },
+      height: {
+        type: String,
+        default: '300px'
+      },
+      bugTypeCounts: {
+        type: Object,
+        default: function () {
+          return {}
+        }
+      }
+    },
+    watch: {
+      bugTypeCounts: {
+        handler (nv, ov) {
+          this.datas = []
+          const names = Object.keys(nv)
+          names.forEach(name => this.datas.push({'name': name, 'value': nv[name]}))
+          this.initChart()
+        },
+        deep: true
+      }
+    },
+    data () {
+      return {
+        chart: null,
+        datas: []
+      }
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.initChart()
+      })
+    },
+    beforeDestroy () {
+      if (!this.chart) {
+        return
+      }
+      this.chart.dispose()
+      this.chart = null
+    },
+    methods:{
+      initChart () {
+        this.chart = echarts.init(document.getElementById('bugTypeCharts'), 'macarons')
+        this.chart.setOption({
+          legend: {
+            orient: 'vertical',
+            left: 'left'
+          },
+          series : [
+            {
+              // name: '访问来源',
+              type: 'pie',    // 设置图表类型为饼图
+              radius: '55%',  // 饼图的半径,外半径为可视区尺寸(容器高宽中较小一项)的 55% 长度。
+              // roseType: 'angle',
+              data: this.datas,
+              label: {
+                normal: {
+                  position: 'inner',
+                  show: true,
+                  formatter: '{d}%'
+                }
+              }
+            }
+          ]
+        })
+      }
+    }
+  }
+</script>
+<style>
+  .chart-title{
+    padding: 5px 8px 15px 8px;
+    font-weight: bold;
+    font-size: 18px;
+  }
+</style>

+ 123 - 0
src/pages/Statistics/components/InfoCard.vue

@@ -0,0 +1,123 @@
+<template>
+  <el-card class="box-card-component">
+    <div style="position:relative;">
+      <mallki class-name="mallki-text" text="任务基本信息"/>
+      <el-form style="padding-top:35px;margin-top: 10px"
+                status-icon ref="ruleForm" label-width="100px" class="demo-ruleForm">
+        <el-form-item label="任务编号:">
+          {{task.taskCode}}
+        </el-form-item>
+        <el-form-item label="任务名称:">
+          {{task.taskName}}
+        </el-form-item>
+        <el-form-item label="项目编号:">
+          {{task.projectCode}}
+        </el-form-item>
+        <el-form-item label="项目名称:">
+          {{task.projectName}}
+        </el-form-item>
+        <el-form-item label="发包单位:">
+          {{task.companyName}}
+        </el-form-item>
+        <el-form-item label="任务状态:">
+          {{task.statusDescr}}
+        </el-form-item>
+        <el-form-item label="创建时间:">
+          {{task.createTime}}
+        </el-form-item>
+        <el-form-item label="结束时间:">
+          {{task.endTime}}
+        </el-form-item>
+        <el-form-item label="有效用例:">
+          {{task.validTestCaseCount}}
+        </el-form-item>
+        <el-form-item label="无效用例:">
+          {{task.testCaseCount - task.validTestCaseCount - task.noExamedTestCaseCount}}
+        </el-form-item>
+        <el-form-item label="待审用例:">
+          {{task.noExamedTestCaseCount}}
+        </el-form-item>
+      </el-form>
+    </div>
+  </el-card>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Mallki from '@/components/TextHoverEffect/Mallki'
+
+export default {
+  components: { Mallki },
+  props:['task'],
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        success: 'success',
+        pending: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      statisticsData: {
+        article_count: 1024,
+        pageviews_count: 1024
+      }
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'name',
+      'avatar',
+      'roles'
+    ])
+  }
+}
+</script>
+
+<style lang="scss" >
+.box-card-component{
+  .el-card__header {
+    padding: 0px!important;
+  }
+}
+</style>
+<style lang="scss" scoped>
+.box-card-component {
+  overflow: scroll;
+  .el-form-item {
+    margin-bottom: 10px !important;
+  }
+
+  .box-card-header {
+    position: relative;
+    height: 220px;
+    img {
+      width: 100%;
+      height: 100%;
+      transition: all 0.2s linear;
+      &:hover {
+        transform: scale(1.1, 1.1);
+        filter: contrast(130%);
+      }
+    }
+  }
+  .mallki-text {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    font-size: 20px;
+    font-weight: bold;
+  }
+  .progress-item {
+    margin-bottom: 10px;
+    font-size: 14px;
+  }
+  @media only screen and (max-width: 1510px){
+    .mallki-text{
+      /*display: none;*/
+    }
+  }
+}
+</style>

+ 235 - 0
src/pages/Statistics/components/PanelGroup.vue

@@ -0,0 +1,235 @@
+<template>
+  <el-row :gutter="30" class="panel-group">
+    <el-col :xs="24" :sm="24" :lg="18">
+      <el-row :gutter="30">
+        <el-col :xs="12" :sm="12" :lg="8" class="card-panel-col">
+          <div class="card-panel">
+            <div class="card-panel-icon-wrapper icon-people">
+              <svg-icon icon-class="peoples" class-name="card-panel-icon" />
+            </div>
+            <div class="card-panel-description">
+              <div class="card-panel-text">
+                参与人数
+              </div>
+              <count-to :start-val="0" :end-val="info.testerCount || 0" :duration="3200" class="card-panel-num" />
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :xs="12" :sm="12" :lg="8" class="card-panel-col">
+          <div class="card-panel">
+            <div class="card-panel-icon-wrapper icon-report">
+              <svg-icon icon-class="documentation" class-name="card-panel-icon" />
+            </div>
+            <div class="card-panel-description">
+              <div class="card-panel-text">
+                测试用例
+              </div>
+              <count-to :start-val="0" :end-val="info.testCaseCount || 0" :duration="2600" class="card-panel-num" />
+            </div>
+          </div>
+        </el-col>
+        <el-col :xs="12" :sm="12" :lg="8" class="card-panel-col">
+          <div class="card-panel">
+            <div class="card-panel-icon-wrapper icon-bug">
+              <svg-icon icon-class="bug" class-name="card-panel-icon" />
+            </div>
+            <div class="card-panel-description">
+              <div class="card-panel-text">
+                缺陷数量
+              </div>
+              <count-to :start-val="0" :end-val="info.defectCount || 0" :duration="3000" class="card-panel-num" />
+            </div>
+          </div>
+        </el-col>
+
+<!--        <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">-->
+<!--          <div class="card-panel">-->
+<!--            <div class="card-panel-icon-wrapper icon-rate">-->
+<!--              <svg-icon icon-class="chart" class-name="card-panel-icon" />-->
+<!--            </div>-->
+<!--            <div class="card-panel-description">-->
+<!--              <div class="card-panel-text">-->
+<!--                覆盖率评估-->
+<!--              </div>-->
+<!--              <count-to :start-val="0" :end-val="info.pageCover*100 || 0"-->
+<!--                        :duration="3600" :decimals="2" class="card-panel-num" />-->
+<!--              <span style="font-size: 20px">%</span>-->
+<!--            </div>-->
+<!--          </div>-->
+<!--        </el-col>-->
+      </el-row>
+    </el-col>
+    <el-col :xs="24" :sm="24" :lg="6" class="card-panel-col">
+      <div class="card-panel">
+        <div class="card-panel-icon-wrapper icon-process">
+          <svg-icon icon-class="example" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            完成度评估
+          </div>
+          <count-to :start-val="0" :end-val="completion || 0" :duration="2600" class="card-panel-num" />
+          <span style="font-size: 20px">%</span>
+        </div>
+<!--        <div class="power-info">powered by 中国科学院</div>-->
+      </div>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import CountTo from 'vue-count-to'
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+
+export default {
+  props: ['info'],
+  components: {
+    CountTo
+  },
+  data: function () {
+    return {
+      completion: 0
+    }
+  },
+  prop: {
+    taskCode: String
+  },
+  created () {
+    this.getCompletion()
+  },
+  methods: {
+    getCompletion () {
+      Http.get(Api.TASK.TASK_COMPLETION.replace('{taskCode}', this.taskCode)).then((res) => {
+        const completionVal = res.data
+        this.completion = completionVal
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取完成度数据失败:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.panel-group {
+  margin-top: 18px;
+
+  .card-panel-col {
+    margin-bottom: 15px;
+  }
+
+  .card-panel {
+    height: 108px;
+    cursor: pointer;
+    font-size: 12px;
+    position: relative;
+    overflow: hidden;
+    color: #666;
+    background: #fff;
+    box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
+    border-color: rgba(0, 0, 0, .05);
+
+    &:hover {
+      .card-panel-icon-wrapper {
+        color: #fff;
+      }
+
+      .icon-people {
+        background: #40c9c6;
+      }
+
+      .icon-report {
+        background: #36a3f7;
+      }
+
+      .icon-bug {
+        background: #f4516c;
+      }
+
+      .icon-rate {
+        background: #ffa30c
+      }
+
+      .icon-process {
+        background: #ff0c51
+      }
+    }
+
+    .icon-people {
+      color: #40c9c6;
+    }
+
+    .icon-report {
+      color: #36a3f7;
+    }
+
+    .icon-bug {
+      color: #f4516c;
+    }
+
+    .icon-rate {
+      color: #ffa30c
+    }
+
+    .icon-process {
+      color: #ff0c51
+    }
+
+    .card-panel-icon-wrapper {
+      float: left;
+      margin: 14px 0 0 14px;
+      padding: 16px;
+      transition: all 0.38s ease-out;
+      border-radius: 6px;
+    }
+
+    .card-panel-icon {
+      float: left;
+      font-size: 48px;
+    }
+
+    .card-panel-description {
+      float: right;
+      font-weight: bold;
+      margin: 26px;
+      margin-left: 0px;
+
+      .card-panel-text {
+        line-height: 18px;
+        color: rgba(0, 0, 0, 0.45);
+        font-size: 16px;
+        margin-bottom: 12px;
+      }
+
+      .card-panel-num {
+        font-size: 20px;
+      }
+    }
+
+    .power-info {
+      position: absolute;
+      bottom: 2px;
+      right: 10px;
+      color: #909399;
+      font-size: 7px;
+    }
+  }
+}
+
+@media (max-width:550px) {
+  .card-panel-icon-wrapper {
+    display: none;
+  }
+  .card-panel-description {
+    margin: 25px 0 0 0 !important;
+    width: 100%;
+    height: 100%;
+    text-align: center;
+  }
+
+}
+</style>

+ 236 - 0
src/pages/Statistics/components/StaffDistributionMap/index.vue

@@ -0,0 +1,236 @@
+<template>
+  <div id="map-wrapper" style="width:100%;height: 650px" v-if="!taskInfo.length"/>
+  <div v-else style="margin: auto">暂无数据生成分布图</div>
+</template>
+
+<script>
+import { geoCoordMap } from './map-data'
+
+export default {
+  name: 'StaffDistributionMap',
+  props: ['taskInfo'],
+  watch: {
+    taskInfo (newVal, oldVal) {
+      let targetCity = newVal.companyCity
+      if (targetCity && targetCity.endsWith('市')) {
+        targetCity = targetCity.substr(0, targetCity.length - 1)
+      }
+      // 初始化起点数据
+      const cityCountMap = new Map()
+      newVal.testers.forEach(tester => {
+        let cityName = tester.city
+        if (cityName && cityName.endsWith('市')) {
+          cityName = cityName.substr(0, cityName.length - 1)
+        }
+        if (cityName) {
+          let val = cityCountMap.get(cityName) || 0
+          val++
+          cityCountMap.set(cityName, val)
+        }
+      })
+      // 初始化飞线数据
+      const fromDatas = []
+      cityCountMap.forEach((v, k) => {
+        fromDatas.push({name: k, value: v})
+      })
+      // 基于准备好的dom,初始化echarts实例
+      const chart = this.$echarts.init(document.getElementById('map-wrapper'))
+      // 地图展现数据
+      const series = []
+
+      const convertData1 = function (data) {
+        const res = []
+        for (let i = 0; i < data.length; i++) {
+          const dataItem = data[i]
+          let fromCityName = dataItem[0].name
+          let toCityName = dataItem[1].name
+          const fromCoord = geoCoordMap[fromCityName]
+          const toCoord = geoCoordMap[toCityName]
+          if (fromCoord && toCoord) {
+            res.push({
+              fromName: fromCityName, toName: toCityName, coords: [fromCoord, toCoord]
+            })
+          }
+        }
+        return res
+      }
+
+      const convertData2 = function (data) {
+        const res = []
+        for (var i = 0; i < data.length; i++) {
+          let cityName = data.name
+          const geoCoord = geoCoordMap[cityName]
+          if (geoCoord) {
+            res.push({
+              name: cityName,
+              value: geoCoord.concat(data.value)
+            })
+          }
+        }
+        return res
+      }
+
+      const getMapDataAction = function () {
+        fromDatas.map((fromData, index) => {
+          let myData = [[{name: fromData.name, value: fromData.value}, {name: targetCity}]]
+          series.push({
+            name: targetCity,
+            type: 'scatter',
+            zlevel: 20,
+            color: '#f00',
+            coordinateSystem: 'geo',
+            symbolSize: 10,
+            itemStyle: {
+              normal: { color: '#f00' }
+            }
+          },
+          {
+            type: 'lines',
+            zlevel: 15,
+            effect: {
+              show: true, period: 4, trailLength: 0, symbol: 'arrow', symbolSize: 7
+            },
+            lineStyle: {
+              normal: { width: 1.2, opacity: 0.6, curveness: 0.2, color: '#F19000' }
+            },
+            data: convertData1(myData)
+          },
+          // 出发点
+          {
+            type: 'effectScatter',
+            coordinateSystem: 'geo',
+            zlevel: 15,
+            rippleEffect: {
+              period: 4, brushType: 'stroke', scale: 4
+            },
+            symbol: 'circle',
+            symbolSize: function (val) {
+              return 4 + val[2] / 10
+            },
+            itemStyle: {
+              normal: { show: false }
+            },
+            tooltip: {
+              show: true,
+              formatter: function (params) {
+                if (params.value) {
+                  return params.name + ' : ' + params.value
+                } else {
+                  return params.name
+                }
+              }
+            },
+            data: [{
+              name: fromData.name, value: fromData.value
+            }]
+          },
+            /* 到达点 */
+          {
+            type: 'effectScatter',
+            coordinateSystem: 'geo',
+            rippleEffect: {
+              period: 4, brushType: 'stroke', scale: 4
+            },
+            zlevel: 15,
+            label: {
+              normal: {
+                show: false
+              }
+            },
+            symbol: 'circle',
+            symbolSize: 15,
+            itemStyle: {
+              normal: {
+                color: '#F19000'
+              }
+            },
+            tooltip: {
+              show: true
+            },
+            data: myData.map(function (dataItem) {
+              return {
+                name: dataItem[1].name,
+                value: geoCoordMap[dataItem[0].name].concat([dataItem[1].name]),
+                tooltip: {
+                  formatter: dataItem[0].name + '--' + dataItem[1].name + ':' + dataItem[0].value
+                }
+              }
+            })
+          })
+          // })
+        })
+
+        const option = {
+          title: {
+            text: '众测工人分布图',
+            padding: 20
+          },
+          backgroundColor: 'white',
+          color: ['#e68b55'],
+          tooltip: {
+            trigger: 'item',
+            show: false
+          },
+          legend: {
+            show: true,
+            orient: 'horizontal',
+            left: '27%',
+            top: '5%',
+            data: ['高线'],
+            textStyle: {
+              color: '#ffffff'
+            },
+            icon: 'circle'
+          },
+          geo: {
+            map: 'china',
+            zlevel: 10,
+            layoutCenter: ['50%', '50%'],
+            roam: false, // 是否允许缩放
+            layoutSize: '100%',
+            zoom: 1.26,
+            label: {
+              normal: {
+                show: true, // 地图上的省份名称是否显示
+                textStyle: {
+                  fontSize: 12,
+                  color: '#43D0D6'
+                }
+              },
+              emphasis: {
+                show: true
+              }
+            },
+            itemStyle: {
+              normal: {
+                color: '#062031',
+                borderWidth: 1.1,
+                borderColor: '#43D0D6'
+              },
+              emphasis: {
+                areaColor: '#43D0D6'
+              }
+            }
+          },
+          series: series
+        }
+        chart.setOption(option)
+        console.log(1, series)
+      }
+      getMapDataAction()
+      window.onresize = () => {
+        // 这里使用箭头函数,避免this指向问题
+        chart && chart.resize()
+      }
+    }
+  },
+  methods: {
+
+  }
+}
+
+</script>
+
+<style scoped>
+
+</style>

+ 77 - 0
src/pages/Statistics/components/StaffDistributionMap/map-data.js

@@ -0,0 +1,77 @@
+// 地图基本数据
+export const geoCoordMap = {
+  '陕西': [109.503789, 35.860026],
+  '西安': [108.946466, 34.347269],
+  '甘肃': [103.832478, 36.065465],
+  '兰州': [103.84044, 36.067321],
+  '新疆': [87.633473, 43.799238],
+  '乌鲁木齐': [87.62444, 43.830763],
+  '内蒙古自治区': [111.772606, 40.823156],
+  '包头': [109.846544, 40.662929],
+  '青海': [101.786462, 36.627159],
+  '西宁': [101.78443, 36.623393],
+  '宁夏': [106.265605, 38.476878],
+  '银川': [106.258602, 38.487834],
+  '四川': [104.073467, 30.577543],
+  '成都': [104.081534, 30.655822],
+  '重庆': [106.558434, 29.568996],
+  '西藏': [91.124342, 29.652894],
+  '拉萨': [91.120789, 29.65005],
+  '云南': [101.592952, 24.864213],
+  '昆明': [102.852448, 24.873998],
+  '贵州': [106.714476, 26.60403],
+  '贵阳': [106.636577, 26.653325],
+  '广西壮族自治区': [108.924274, 23.552255],
+  '南宁': [108.373451, 22.822607],
+  '山西': [112.515496, 37.866566],
+  '太原': [112.534919, 37.873219],
+  '河南': [113.65, 33.75],
+  '郑州': [113.631419, 34.753439],
+  '湖北': [112.410562, 31.209316],
+  '武汉': [114.311582, 30.598467],
+  '湖南': [111.720664, 27.695864],
+  '长沙': [112.945473, 28.234889],
+  '江西': [115.676082, 27.757258],
+  '南昌': [115.864589, 28.689455],
+  '安徽': [117.33054, 31.734294],
+  '合肥': [117.233443, 31.826578],
+  '南京': [118.78, 32.04],
+  '请选择省份': [118.78, 32.04],
+  '江苏': [118.78, 32.04],
+  '山东': [117,36.65],
+  '上海': [121.480539, 31.235929],
+  '浙江': [120.159533, 30.271548],
+  '杭州': [120.215503, 30.253087],
+  '广东': [113.394818, 23.408004],
+  '广州': [113.271431, 23.135336],
+  '北京': [116.413384, 39.910925],
+  '天津': [117.209523, 39.093668],
+  '河北': [117.220297, 39.173149],
+  '唐山': [118.186459, 39.636584],
+  '黑龙江':[127.3,46.28],
+  '辽宁':[123.38,41.8],
+  '福建':[119.3,26.08],
+  '吉林':[125.3,43.88]
+}
+// 初始化飞线数据
+export const XAData = [
+  [{ name: '乌鲁木齐' }, { name: '南京' }],
+  [{ name: '西宁' }, { name: '南京' }],
+  [{ name: '兰州' }, { name: '南京' }],
+  [{ name: '银川' }, { name: '南京' }],
+  [{ name: '包头' }, { name: '南京' }],
+  [{ name: '太原' }, { name: '南京' }],
+  [{ name: '拉萨' }, { name: '南京' }],
+  [{ name: '成都' }, { name: '南京' }],
+  [{ name: '重庆' }, { name: '南京' }],
+  [{ name: '昆明' }, { name: '南京' }],
+  [{ name: '贵阳' }, { name: '南京' }],
+  [{ name: '广州' }, { name: '南京' }],
+  [{ name: '长沙' }, { name: '南京' }]
+]
+// 获取起始节点
+export const getFromData = () => {
+  return XAData.map(item => {
+    return item[0].name
+  })
+}

+ 81 - 0
src/pages/Statistics/components/TodoList/Todo.vue

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

+ 320 - 0
src/pages/Statistics/components/TodoList/index.scss

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

+ 127 - 0
src/pages/Statistics/components/TodoList/index.vue

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

+ 57 - 0
src/pages/Statistics/components/TransactionTable.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-table :data="workList" style="width: 100%;height:100%;padding-top: 15px;" @row-click="toTesterTaskPage">
+    <el-table-column label="用户名" min-width="100" align="center">
+      <template slot-scope="scope" class="no-wrap">
+        <div class="no-wrap">{{ scope.row.name }}</div>
+      </template>
+    </el-table-column>
+    <el-table-column label="得分" min-width="100" align="center">
+      <template slot-scope="scope" class="no-wrap">
+        {{ scope.row.score === -1 ? '未评分' :  scope.row.score}}
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+  // import { transactionList } from '@/api/remote-search'
+
+  export default {
+    props: ['list', 'taskCode'],
+    watch: {
+      list: {
+        handler(nv, ov) {
+          this.workList = nv.length&&nv.length > 13 ? nv.slice(0, 13) : nv
+        },
+        deep: true,
+      }
+    },
+    data () {
+      return {
+        workList: []
+      }
+    },
+    methods: {
+      toTesterTaskPage(row) {
+        this.$router.push({
+          name: 'TesterTaskPage',
+          params: {userId: row.id, taskCode: this.taskCode}
+        })
+      }
+    }
+  }
+</script>
+<style lang="less">
+  .no-wrap {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -o-text-overflow: ellipsis;
+    -webkit-text-overflow: ellipsis;
+    -moz-text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .el-table tbody tr:hover {
+    cursor: pointer!important;
+  }
+</style>

+ 55 - 0
src/pages/Statistics/components/mixins/resize.js

@@ -0,0 +1,55 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      $_sidebarElm: null,
+      $_resizeHandler: null
+    }
+  },
+  mounted() {
+    this.$_resizeHandler = debounce(() => {
+      if (this.chart) {
+        this.chart.resize()
+      }
+    }, 100)
+    this.$_initResizeEvent()
+    this.$_initSidebarResizeEvent()
+  },
+  beforeDestroy() {
+    this.$_destroyResizeEvent()
+    this.$_destroySidebarResizeEvent()
+  },
+  // to fixed bug when cached by keep-alive
+  // https://github.com/PanJiaChen/vue-element-admin/issues/2116
+  activated() {
+    this.$_initResizeEvent()
+    this.$_initSidebarResizeEvent()
+  },
+  deactivated() {
+    this.$_destroyResizeEvent()
+    this.$_destroySidebarResizeEvent()
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_initResizeEvent() {
+      window.addEventListener('resize', this.$_resizeHandler)
+    },
+    $_destroyResizeEvent() {
+      window.removeEventListener('resize', this.$_resizeHandler)
+    },
+    $_sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.$_resizeHandler()
+      }
+    },
+    $_initSidebarResizeEvent() {
+      this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+      this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    $_destroySidebarResizeEvent() {
+      this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+    }
+  }
+}

+ 101 - 0
src/pages/TestCase/components/defect_detail.vue

@@ -0,0 +1,101 @@
+<template>
+  <el-form ref="defectDetail" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="用例编号">
+      <span>{{defectData.testCaseCode}}</span>
+    </el-form-item>
+    <el-form-item label="缺陷编号">
+      <span>{{defectData.code}}</span>
+    </el-form-item>
+    <el-form-item label="缺陷描述">
+      <expend-text :text="defectData.descr"></expend-text>
+    </el-form-item>
+    <el-form-item label="严重等级">
+      <span>{{toSeriousnessCn(defectData.seriousness)}}</span>
+    </el-form-item>
+    <el-form-item label="优先级">
+      <span>{{toPriorityCn(defectData.priority)}}</span>
+    </el-form-item>
+    <el-form-item label="缺陷类型">
+      <span>{{toDefectTypeCn(defectData.defectType)}}</span>
+    </el-form-item>
+    <el-form-item label="前置条件">
+      <expend-text :text="defectData.preconditions"></expend-text>
+    </el-form-item>
+    <el-form-item label="环境配置">
+      <expend-text :text="defectData.envConfig"></expend-text>
+    </el-form-item>
+    <el-form-item label="操作步骤">
+      <expend-text :text="defectData.opeSteps"></expend-text>
+    </el-form-item>
+    <el-form-item label="输入数据">
+      <expend-text :text="defectData.inputDatas"></expend-text>
+    </el-form-item>
+    <el-form-item label="预期结果">
+      <expend-text :text="defectData.expectedResult"></expend-text>
+    </el-form-item>
+    <el-form-item label="测试结果">
+      <expend-text :text="defectData.testResult"></expend-text>
+    </el-form-item>
+    <el-form-item label="其他说明">
+      <expend-text :text="defectData.others"></expend-text>
+    </el-form-item>
+    <el-form-item label="附件" prop="files">
+      <span v-if="defectData.files.length === 0">无</span>
+      <div v-for="file in defectData.files">
+        <el-link :key="file" :href="file" target="_blank">{{toFileName(file)}}</el-link>
+      </div>
+    </el-form-item>
+    <el-form-item label="截图" prop="screenshots">
+      <span v-if="defectData.screenshots.length === 0">无</span>
+      <el-image v-for="screenshot in defectData.screenshots" :key="screenshot"
+        style="width: 130px; height: 130px; margin-left: 10px;"
+        :src="screenshot"
+        :preview-src-list="[screenshot]">
+      </el-image>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import ExpendText from '@/components/text/ExpendText'
+import TestCaseUtils from '../utils'
+import {toFileName} from '@/js/file'
+
+export default {
+  name: 'DefectDetail',
+  components: {ExpendText},
+  props: {
+    defectData: {
+      type: Object,
+      default: function () {
+        return {
+          id: undefined,
+          testCaseCode: '',
+          taskCode: '',
+          descr: '',
+          preconditions: '',
+          envConfig: '',
+          priority: undefined,
+          seriousness: undefined,
+          defectType: undefined,
+          opeSteps: '',
+          inputDatas: '',
+          expectedResult: '',
+          others: '',
+          testResult: '',
+          files: [],
+          screenshots: []
+        }
+      }
+    }
+  },
+  methods: {
+    ...TestCaseUtils,
+    toFileName
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 215 - 0
src/pages/TestCase/components/defect_form.vue

@@ -0,0 +1,215 @@
+<template>
+  <el-form ref="defectForm" :rules="rules" :model="defect" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="用例编号" prop="testCaseCode">
+      <el-input v-model="defect.testCaseCode" disabled/>
+    </el-form-item>
+    <el-form-item label="描述" prop="descr">
+      <el-input type="textarea" v-model="defect.descr" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="严重等级" prop="seriousness">
+      <el-select v-model="defect.seriousness">
+        <el-option value="VERY_HIGH" label="致命"/>
+        <el-option value="HIGH" label="严重"/>
+        <el-option value="MID" label="一般"/>
+        <el-option value="LOW" label="轻微"/>
+        <el-option value="VERY_LOW" label="建议"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="优先级" prop="priority">
+      <el-select v-model="defect.priority">
+        <el-option value="HIGH" label="高"/>
+        <el-option value="MID" label="中"/>
+        <el-option value="LOW" label="低"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="缺陷类型" prop="defectType">
+      <el-select v-model="defect.defectType">
+        <el-option value="FUNCTIONALITY" label="功能性"/>
+        <el-option value="COMPATIBILITY" label="兼容性"/>
+        <el-option value="INFORMATION_SECURITY" label="信息安全性"/>
+        <el-option value="RELIABILITY" label="可靠性"/>
+        <el-option value="USE" label="易用性"/>
+        <el-option value="PERFORMANCE" label="性能效率"/>
+        <el-option value="PORTABILITY" label="可移植性"/>
+        <el-option value="MAINTAINABILITY" label="维护性"/>
+        <el-option value="OTHER" label="其他"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="前置条件" prop="preconditions">
+      <el-input type="textarea" v-model="defect.preconditions" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="环境配置" prop="envConfig">
+      <el-input type="textarea" v-model="defect.envConfig" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="操作步骤" prop="opeSteps">
+      <el-input type="textarea" v-model="defect.opeSteps" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="输入数据" prop="inputDatas">
+      <el-input type="textarea" v-model="defect.inputDatas" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="预期结果" prop="expectedResult">
+      <el-input type="textarea" v-model="defect.expectedResult" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="测试结果" prop="testResult">
+      <el-input type="textarea" v-model="defect.testResult" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="其他说明" prop="others">
+      <el-input type="textarea" v-model="defect.others" :autosize="{ minRows: 2}"/>
+    </el-form-item>
+    <el-form-item label="附件" prop="files">
+      <file-upload :countLimit="countLimit" :files="defect.files"></file-upload>
+    </el-form-item>
+    <el-form-item label="截图" prop="screenshots">
+      <img-upload :countLimit="countLimit" :files="defect.screenshots"></img-upload>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import ElDragSelect from '@/components/DragSelect'
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import FileUpload from '@/components/file/FileUpload'
+import ImgUpload from '@/components/file/ImgUpload'
+import {deepClone} from '@/utils/datas'
+
+export default {
+  name: 'DefectForm',
+  components: { ElDragSelect, FileUpload, ImgUpload },
+  data: function () {
+    return {
+      countLimit: 3,
+      rules: {
+        descr: [
+          {required: true, message: '描述不可为空', trigger: 'blur'}
+        ],
+        seriousness: [
+          {required: true, message: '严重等级不可为空'}
+        ],
+        priority: [
+          {required: true, message: '优先级不可为空'}
+        ],
+        defectType: [
+          {required: true, message: '缺陷类型不可为空'}
+        ],
+        testResult: [
+          {required: true, message: '测试结果不可为空'}
+        ]
+      },
+      loading: false,
+      testCases: [],
+      defect: this.init()
+    }
+  },
+  props: {
+    defectData: {
+      type: Object,
+      required: true,
+      default: this.init
+    }
+  },
+  watch: {
+    defectData: {
+      immediate: true,
+      handler (nv, ov) {
+        deepClone(this.defect, nv)
+      },
+      deep: true
+    }
+  },
+  methods: {
+    init () {
+      return {
+        id: undefined,
+        testCaseCode: '',
+        taskCode: '',
+        descr: '',
+        preconditions: '',
+        envConfig: '',
+        priority: undefined,
+        seriousness: undefined,
+        defectType: undefined,
+        opeSteps: '',
+        inputDatas: '',
+        expectedResult: '',
+        others: '',
+        testResult: '',
+        files: [],
+        screenshots: []
+      }
+    },
+    submitForm (callback) {
+      this.$refs['defectForm'].validate(valid => {
+        if (valid) {
+          this.showLoading()
+          const newDefect = {
+            testCaseCode: this.defect.testCaseCode,
+            taskCode: this.defect.taskCode,
+            descr: this.defect.descr,
+            preconditions: this.defect.preconditions,
+            envConfig: this.defect.envConfig,
+            priority: this.defect.priority,
+            seriousness: this.defect.seriousness,
+            defectType: this.defect.defectType,
+            opeSteps: this.defect.opeSteps,
+            inputDatas: this.defect.inputDatas,
+            expectedResult: this.defect.expectedResult,
+            others: this.defect.others,
+            testResult: this.defect.testResult,
+            files: this.defect.files,
+            screenshots: this.defect.screenshots
+          }
+          if (this.defect.id && this.defect.id > 0) {
+            newDefect['id'] = this.defect.id
+          }
+          console.log(newDefect)
+          let url
+          let submitDataMethod
+          if (newDefect['id']) {
+            url = Api.TESTCASE.UPDATE_DEFECT.replace('{id}', newDefect['id'])
+            submitDataMethod = Http.put
+          } else {
+            url = Api.TESTCASE.ADD_DEFECT
+            submitDataMethod = Http.post
+          }
+          submitDataMethod(url, newDefect).then((res) => {
+            console.log(res)
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交缺陷失败:' + res.msg)
+            } else {
+              notify('success', '提交成功')
+              if (!this.defect.id) {
+                deepClone(this.defect, this.defectData)
+                this.clearValidate()
+              }
+              callback()
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '缺陷创建失败:' + error)
+          })
+          // 提交 report
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    showLoading () {
+      this.loading = true
+    },
+    hideLoading () {
+      this.loading = false
+    },
+    clearValidate (props = []) {
+      this.$refs['defectForm'].clearValidate()
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

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