Browse Source

修改了测试用例管理的部分功能和新增了任务仪表盘功能

linyk 2 years ago
parent
commit
6bed8e53cc
100 changed files with 5291 additions and 1217 deletions
  1. 1 1
      build/build.js
  2. 9 0
      build/webpack.base.conf.js
  3. 2 1
      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. 127 126
      package-lock.json
  9. 5 2
      package.json
  10. 62 0
      src/components/SvgIcon/index.vue
  11. 113 0
      src/components/TextHoverEffect/Mallki.vue
  12. 3 3
      src/components/file/FileUpload.vue
  13. 75 0
      src/components/project/ProjectSearch.vue
  14. 90 0
      src/components/project/TaskSearch.vue
  15. 22 4
      src/components/task/Task.vue
  16. 1 1
      src/components/text/ExpendText.vue
  17. 9 0
      src/icons/index.js
  18. 1 0
      src/icons/svg/404.svg
  19. 1 0
      src/icons/svg/bug.svg
  20. 1 0
      src/icons/svg/chart.svg
  21. 1 0
      src/icons/svg/clipboard.svg
  22. 1 0
      src/icons/svg/component.svg
  23. 0 0
      src/icons/svg/dashboard.svg
  24. 1 0
      src/icons/svg/documentation.svg
  25. 1 0
      src/icons/svg/drag.svg
  26. 1 0
      src/icons/svg/edit.svg
  27. 1 0
      src/icons/svg/education.svg
  28. 1 0
      src/icons/svg/email.svg
  29. 1 0
      src/icons/svg/example.svg
  30. 1 0
      src/icons/svg/excel.svg
  31. 1 0
      src/icons/svg/exit-fullscreen.svg
  32. 1 0
      src/icons/svg/eye-open.svg
  33. 1 0
      src/icons/svg/eye.svg
  34. 0 0
      src/icons/svg/form.svg
  35. 1 0
      src/icons/svg/fullscreen.svg
  36. 1 0
      src/icons/svg/guide.svg
  37. 1 0
      src/icons/svg/icon.svg
  38. 1 0
      src/icons/svg/international.svg
  39. 1 0
      src/icons/svg/language.svg
  40. 1 0
      src/icons/svg/link.svg
  41. 1 0
      src/icons/svg/list.svg
  42. 1 0
      src/icons/svg/lock.svg
  43. 1 0
      src/icons/svg/message.svg
  44. 1 0
      src/icons/svg/money.svg
  45. 1 0
      src/icons/svg/nested.svg
  46. 1 0
      src/icons/svg/password.svg
  47. 1 0
      src/icons/svg/pdf.svg
  48. 1 0
      src/icons/svg/people.svg
  49. 1 0
      src/icons/svg/peoples.svg
  50. 0 0
      src/icons/svg/qq.svg
  51. 1 0
      src/icons/svg/search.svg
  52. 0 0
      src/icons/svg/shopping.svg
  53. 1 0
      src/icons/svg/size.svg
  54. 1 0
      src/icons/svg/skill.svg
  55. 1 0
      src/icons/svg/star.svg
  56. 1 0
      src/icons/svg/tab.svg
  57. 1 0
      src/icons/svg/table.svg
  58. 1 0
      src/icons/svg/theme.svg
  59. 1 0
      src/icons/svg/tree-table.svg
  60. 1 0
      src/icons/svg/tree.svg
  61. 1 0
      src/icons/svg/user.svg
  62. 1 0
      src/icons/svg/wechat.svg
  63. 1 0
      src/icons/svg/zip.svg
  64. 22 0
      src/icons/svgo.yml
  65. 22 2
      src/js/api.js
  66. 5 1
      src/main.js
  67. 161 0
      src/pages/Statistics/TaskStatistics.vue
  68. 131 0
      src/pages/Statistics/TaskStatistics_bak.vue
  69. 113 0
      src/pages/Statistics/components/BugLevelChart.vue
  70. 105 0
      src/pages/Statistics/components/BugTimeCountChart.vue
  71. 99 0
      src/pages/Statistics/components/BugTypeChart.vue
  72. 123 0
      src/pages/Statistics/components/InfoCard.vue
  73. 235 0
      src/pages/Statistics/components/PanelGroup.vue
  74. 236 0
      src/pages/Statistics/components/StaffDistributionMap/index.vue
  75. 77 0
      src/pages/Statistics/components/StaffDistributionMap/map-data.js
  76. 81 0
      src/pages/Statistics/components/TodoList/Todo.vue
  77. 320 0
      src/pages/Statistics/components/TodoList/index.scss
  78. 127 0
      src/pages/Statistics/components/TodoList/index.vue
  79. 57 0
      src/pages/Statistics/components/TransactionTable.vue
  80. 55 0
      src/pages/Statistics/components/mixins/resize.js
  81. 76 59
      src/pages/TestCase/components/defect_form.vue
  82. 292 0
      src/pages/TestCase/components/defect_list.vue
  83. 358 0
      src/pages/TestCase/components/test_case_list.vue
  84. 298 0
      src/pages/TestCase/components/test_env_list.vue
  85. 244 0
      src/pages/TestCase/components/test_tool_list.vue
  86. 6 2
      src/pages/TestCase/components/testcase_detail.vue
  87. 128 68
      src/pages/TestCase/components/testcase_form.vue
  88. 114 378
      src/pages/TestCase/exam_testcases.vue
  89. 156 558
      src/pages/TestCase/testcases.vue
  90. 5 5
      src/pages/TestCase/utils/index.js
  91. 98 0
      src/pages/Tester/cloud-data.js
  92. 102 0
      src/pages/Tester/components/BarChart.vue
  93. 111 0
      src/pages/Tester/components/BoxCard.vue
  94. 114 0
      src/pages/Tester/components/BugLevelChart.vue
  95. 116 0
      src/pages/Tester/components/InfoCard.vue
  96. 135 0
      src/pages/Tester/components/LineChart.vue
  97. 181 0
      src/pages/Tester/components/PanelGroup.vue
  98. 79 0
      src/pages/Tester/components/PieChart.vue
  99. 100 0
      src/pages/Tester/components/RaddarChart.vue
  100. 33 0
      src/pages/Tester/components/TimeLine.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]'
+        }
       }
     ]
   },

+ 2 - 1
config/dev.env.js

@@ -16,5 +16,6 @@ module.exports = {
   ENV_CONFIG: "'dev'",
   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/api/', // 设置你调用的接口域名和端口号
+    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
+ 127 - 126
package-lock.json


+ 5 - 2
package.json

@@ -9,21 +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",

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

+ 3 - 3
src/components/file/FileUpload.vue

@@ -10,7 +10,7 @@
     :before-upload="beforeFileUpload"
     :on-remove="handleFileRemove"
     :file-list="fileList"
-    :on-error="imgUploadError"
+    :on-error="fileUploadError"
     :disabled="disabled"
   >
     <i class="el-icon-upload"></i>
@@ -85,9 +85,9 @@ export default {
       this.fileList.push(file)
       this.files.push(response)
     },
-    imgUploadError (err, file, fileList) { // 图片上传失败调用
+    fileUploadError (err, file, fileList) { // 文件上传失败调用
       console.log(err)
-      this.$message.error('上传图片失败!')
+      this.$message.error('上传文件失败!')
     },
     handleFileRemove (file) {
       console.log(this.fileList)

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

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

@@ -0,0 +1,90 @@
+<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: {
+    selectedProjectCode: {
+      type: String,
+      default: ''
+    },
+    firstSelectedTaskCode: {
+      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) {
+      console.log(projectCode)
+      Http.get(Api.TASK.GET_SIMPLE_DATAS_BY_PROJECT.replace('{projectCode}', projectCode)).then((res) => {
+        this.tasks = res.data
+        this.searchTasks = res.data
+        if (!this.firstSelectedTaskCode) {
+          this.selectedTaskData = this.tasks[0]
+          this.selectedTaskCode = this.selectedTaskData.code
+        } else {
+          this.firstSelectedTaskCode = ''
+          this.selectedTaskData = this.searchTasks.find(task => task.code === this.selectedTaskCode)
+        }
+      }).catch((error) => {
+        notify('error', '获取任务列表数据失败:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 22 - 4
src/components/task/Task.vue

@@ -226,8 +226,11 @@
               <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="toTestCases()">
-                用例管理
+              <el-button v-if="taskOperationControl.testCaseManage" type="primary" size="mini" @click="toTestCases()">
+                众测执行
+              </el-button>
+              <el-button v-if="taskOperationControl.testCaseExam" type="primary" size="mini" @click="toTestCasesExam()">
+                众测审核
               </el-button>
               <el-button v-if="taskOperationControl.taskDemonstrate" type="success" size="mini" @click="gotoDataboard()">
                 任务面板
@@ -333,7 +336,9 @@ export default {
         uploadReport: false,
         writeReport: false,
         taskDemonstrate: false,
-        taskRecommend: false
+        taskRecommend: false,
+        testCaseExam: false,
+        testCaseManage: false
       },
       crowdReportUrl: '',
       wordCloud:[],
@@ -673,7 +678,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);
@@ -997,6 +1002,19 @@ export default {
           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
+        }
+      })
     }
   }
 }

+ 1 - 1
src/components/text/ExpendText.vue

@@ -23,7 +23,7 @@ export default {
     },
     text: {
       type: String,
-      required: true
+      default: ''
     }
   },
   computed: {

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

+ 22 - 2
src/js/api.js

@@ -30,7 +30,9 @@ export default {
     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'
+    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}/',
@@ -114,10 +116,28 @@ export default {
     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}/'
+    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 - 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>

+ 131 - 0
src/pages/Statistics/TaskStatistics_bak.vue

@@ -0,0 +1,131 @@
+<template>
+  <div>
+    <el-row :gutter="24">
+      <el-col :span="5">
+        <el-card>
+          <p slot="header">参与人数</p>
+          <div>
+            <p><span class="card-big-font">11</span></p>
+            <!--          <p style="font-size: 14px;padding-top: 10px;">-->
+            <!--            总销售量-->
+            <!--            <span class="card-span-color"><span>2,029台</span><el-icon type="md-trending-up" /></span>-->
+            <!--          </p>-->
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="5">
+        <el-card>
+          <p slot="header">用例数量</p>
+          <div>
+            <p><span class="card-big-font">400</span></p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="5">
+        <el-card>
+          <p slot="header">缺陷数量</p>
+          <div>
+            <p><span class="card-big-font">310</span></p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="5">
+        <el-card>
+          <p slot="header">覆盖率评估</p>
+          <div>
+            <p><span class="card-big-font">54.63 %</span></p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="4">
+        <el-card>
+          <p slot="header">完成度评估</p>
+          <div>
+            <p><span class="card-big-font">100 %</span></p>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+    <el-row :gutter="24" style="margin-top: 10px;">
+      <el-col :span="5">
+        <el-card>
+          <p slot="header">任务基本信息</p>
+          <div>
+            <el-form label-position="right" label-width="100px">
+              <el-form-item label="任务编号:">
+                TASK11111111
+              </el-form-item>
+              <el-form-item label="任务名称:">
+                基于鲲鹏生态的表达能力评测系统众测-GNCS
+              </el-form-item>
+              <el-form-item label="所属项目:">
+                PROJ11111111
+              </el-form-item>
+              <el-form-item label="所属公司:">
+                XXXXXXXXXXXXXXXXXXXXXXX公司
+              </el-form-item>
+              <el-form-item label="开始时间:">
+                2022-04-13
+              </el-form-item>
+              <el-form-item label="结束时间:">
+                2022-06-15
+              </el-form-item>
+              <el-form-item label="任务状态:">
+                执行中
+              </el-form-item>
+            </el-form>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="14">
+        <el-card>
+        </el-card>
+      </el-col>
+      <el-col :span="5">
+        <el-card>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TaskStatistics'
+}
+</script>
+
+<style scoped>
+  .card-big-font {
+    font-size: 36px;
+    color: #666;
+    line-height: 36px;
+    padding: 5px 0 10px;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    white-space: nowrap;
+    margin-bottom: 5px;
+  }
+
+  .card-span-color {
+    position: absolute;
+    right: 15px;
+  }
+</style>
+<style>
+  .el-card__header {
+    padding-top: 2px;
+    padding-bottom: 2px;
+  }
+  .el-card__body {
+    padding-top: 3px;
+    padding-bottom: 3px;
+  }
+  .el-row {
+    margin-left: 0px!important;
+    margin-right: 0px!important;;
+  }
+  label {
+    font-weight: 700;
+  }
+</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)
+    }
+  }
+}

+ 76 - 59
src/pages/TestCase/components/defect_form.vue

@@ -1,29 +1,29 @@
 <template>
-  <el-form ref="defectForm" :rules="rules" :model="defectData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+  <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="defectData.testCaseCode" disabled/>
+      <el-input v-model="defect.testCaseCode" disabled/>
     </el-form-item>
     <el-form-item label="描述" prop="descr">
-      <el-input type="textarea" v-model="defectData.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="defectData.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 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="defectData.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="defectData.defectType">
+      <el-select v-model="defect.defectType">
         <el-option value="FUNCTIONALITY" label="功能性"/>
         <el-option value="COMPATIBILITY" label="兼容性"/>
         <el-option value="INFORMATION_SECURITY" label="信息安全性"/>
@@ -36,31 +36,31 @@
       </el-select>
     </el-form-item>
     <el-form-item label="前置条件" prop="preconditions">
-      <el-input type="textarea" v-model="defectData.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="defectData.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="defectData.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="defectData.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="defectData.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="defectData.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="defectData.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="defectData.files"></file-upload>
+      <file-upload :countLimit="countLimit" :files="defect.files"></file-upload>
     </el-form-item>
     <el-form-item label="截图" prop="screenshots">
-      <img-upload :countLimit="countLimit" :files="defectData.screenshots"></img-upload>
+      <img-upload :countLimit="countLimit" :files="defect.screenshots"></img-upload>
     </el-form-item>
   </el-form>
 </template>
@@ -72,6 +72,7 @@ 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',
@@ -96,58 +97,71 @@ export default {
           {required: true, message: '测试结果不可为空'}
         ]
       },
-      loading: false
+      loading: false,
+      testCases: [],
+      defect: this.init()
     }
   },
   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: []
-        }
-      }
+      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.defectData.testCaseCode,
-            taskCode: this.defectData.taskCode,
-            descr: this.defectData.descr,
-            preconditions: this.defectData.preconditions,
-            envConfig: this.defectData.envConfig,
-            priority: this.defectData.priority,
-            seriousness: this.defectData.seriousness,
-            defectType: this.defectData.defectType,
-            opeSteps: this.defectData.opeSteps,
-            inputDatas: this.defectData.inputDatas,
-            expectedResult: this.defectData.expectedResult,
-            others: this.defectData.others,
-            testResult: this.defectData.testResult,
-            files: this.defectData.files,
-            screenshots: this.defectData.screenshots
+            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.defectData.id && this.defectData.id > 0) {
-            newDefect['id'] = this.defectData.id
+          if (this.defect.id && this.defect.id > 0) {
+            newDefect['id'] = this.defect.id
           }
           console.log(newDefect)
           let url
@@ -165,8 +179,11 @@ export default {
             if (res.code !== 20000) {
               notify('error', '提交缺陷失败:' + res.msg)
             } else {
-              this.defectData.id = res.data
               notify('success', '提交成功')
+              if (!this.defect.id) {
+                deepClone(this.defect, this.defectData)
+                this.clearValidate()
+              }
               callback()
             }
           }).catch((error) => {

+ 292 - 0
src/pages/TestCase/components/defect_list.vue

@@ -0,0 +1,292 @@
+<template>
+  <div>
+    <el-table
+      :data="defects"
+      border
+      fit
+      style="width: 100%"
+      v-loading="listLoading"
+    >
+      <el-table-column label="编号" align="center" min-width="15%">
+        <template slot-scope="{row}">
+          <span>{{ row.code }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="缺陷描述" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.descr }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="严重等级" align="center" min-width="12%">
+        <template slot-scope="{row}">
+          <span>{{ toSeriousnessCn(row.seriousness) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="优先级" align="center" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ toPriorityCn(row.priority) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="缺陷类型" align="center" min-width="12%">
+        <template slot-scope="{row}">
+          <span>{{ toDefectTypeCn(row.defectType) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作步骤" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.opeSteps }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="输入数据" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.inputDatas }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.expectedResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.testResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
+        <template slot-scope="{row,$index}">
+          <i class="el-icon-tickets mini-margin" @click="handleDetail(row)" v-if="!testCaseCanEdit" style="cursor: pointer;" title="查看详情"></i>
+          <i class="el-icon-edit mini-margin " @click="handleUpdate(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
+          <i class="el-icon-delete mini-margin " @click="handleDelete(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
+          <i class="el-icon-document-copy mini-margin " @click="handleCopy(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="复制"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-if="!isContained" v-show="total>0" :total="total" :page.sync="listQueryParam.pageNo" :limit.sync="listQueryParam.pageSize" @pagination="getList" />
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
+      <defect-form ref="defectForm" style="width: 600px;" :defect-data="defectData"></defect-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submit()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="缺陷详情" :visible.sync="detailDialogFormVisible" :close-on-click-modal="false">
+      <defect-detail ref="defectDetail" style="width: 600px;" :defect-data="defectData"></defect-detail>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="detailDialogFormVisible = false">
+          取消
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import TestCaseUtils from '../utils'
+import {deepClone} from '@/utils/datas'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+import DefectForm from './defect_form'
+import DefectDetail from './defect_detail'
+import Pagination from '@/components/Pagination'
+import {mapGetters} from 'vuex'
+
+export default {
+  name: 'DefectList',
+  components: { DefectForm, DefectDetail, Pagination },
+  data: function () {
+    return {
+      detailDialogFormVisible: false,
+      dialogFormVisible: false,
+      textMap: {
+        create: '新建缺陷',
+        update: '编辑缺陷'
+      },
+      defectData: {},
+      listLoading: false,
+      dialogStatus: '',
+      listQueryParam: {
+        pageNo: 1,
+        pageSize: 20,
+        selectedTaskCode: this.selectedTaskCode,
+        selectedUserId: this.selectedUserId
+      },
+      total: 0,
+    }
+  },
+  props: {
+    isContained: {
+      type: Boolean,
+      default: false
+    },
+    defects: {
+      type: Array,
+      default: function () {
+        return []
+      }
+    },
+    testCaseCanEdit: {
+      type: Boolean,
+      default: false
+    },
+    selectedTaskCode: {
+      type: String,
+      default: ''
+    },
+    selectedUserId: {
+      type: Number,
+      default: 0
+    }
+  },
+  created () {
+    this.initDefectData()
+    if (!this.isContained) {
+      this.getList()
+    }
+  },
+  methods: {
+    ...TestCaseUtils,
+    ...mapGetters(['getRefreshTestCaseListFunc', 'getRefreshDefectListFunc']),
+    handleDetail (row) {
+      console.log(row)
+      deepClone(this.defectData, row)
+      this.detailDialogFormVisible = true
+    },
+    handleUpdate (row) {
+      this.dialogStatus = 'update_defect'
+      this.dialogFormVisible = true
+      deepClone(this.defectData, row)
+      this.$nextTick(() => {
+        this.$refs['defectForm'].clearValidate()
+      })
+      console.log(this.defectData)
+    },
+    handleDelete (row, index) {
+      this.$confirm('您确定要删除该条数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.delete(Api.TESTCASE.DELETE_DEFECT.replace('{id}', row.id), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            notify('success', '删除成功')
+            this.getRefreshTestCaseListFunc()()
+            this.getRefreshDefectListFunc()()
+          } else {
+            notify('error', '删除失败:' + res.data)
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '删除失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    handleCopy (row) {
+      this.dialogStatus = 'create_defect'
+      this.dialogFormVisible = true
+      deepClone(this.defectData, row)
+      this.defectData.id = undefined
+      this.$nextTick(() => {
+        this.$refs['defectForm'].clearValidate()
+      })
+      console.log(this.defectData)
+    },
+    handleCreate (testCase) {
+      this.initDefectData()
+      if (testCase) {
+        this.defectData.taskCode = this.listQueryParam.selectedTaskCode
+        this.defectData.testCaseCode = testCase.code
+        this.defectData.priority = testCase.priority
+        this.defectData.preconditions = testCase.preconditions
+        this.defectData.envConfig = testCase.envConfig
+        this.defectData.opeSteps = testCase.opeSteps
+        this.defectData.inputDatas = testCase.inputDatas
+        this.defectData.expectedResult = testCase.expectedResult
+        this.defectData.testResult = testCase.testResult
+      }
+      this.dialogStatus = 'create_defect'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['defectForm'].clearValidate()
+      })
+    },
+    submit () {
+      this.$refs.defectForm.submitForm(() => {
+        this.getRefreshDefectListFunc()()
+        this.getRefreshTestCaseListFunc()()
+      })
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    initDefectData () {
+      this.defectData = {
+        descr: '',
+        envConfig: '',
+        expectedResult: '',
+        files: [],
+        id: undefined,
+        inputDatas: '',
+        opeSteps: '',
+        others: '',
+        preconditions: '',
+        priority: '',
+        seriousness: '',
+        defectType: '',
+        screenshots: [],
+        taskCode: '',
+        testCaseCode: '',
+        testResult: ''
+      }
+    },
+    getList () {
+      this.showListLoading()
+      let url = Api.TESTCASE.USER_DEFECTS.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
+        .replace('{committerId}', this.listQueryParam.selectedUserId)
+        .replace('{pageNo}', this.listQueryParam.pageNo - 1)
+        .replace('{pageSize}', this.listQueryParam.pageSize)
+      Http.get(url).then((res) => {
+        const defectPage = res.data
+        console.log(defectPage)
+        this.total = defectPage.totalCount
+        this.listQueryParam.pageNo = defectPage.pageNo + 1
+        this.listQueryParam.pageSize = defectPage.pageSize
+        this.defects.splice(0, this.defects.length)
+        defectPage.datas.forEach(defect => {
+          this.defects.push(defect)
+        })
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取缺陷数据失败:' + error.data.message)
+      })
+      this.hideListLoading()
+    },
+    clearList () {
+      this.defects.splice(0, this.defects.length)
+      this.total = 0
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mini-margin {
+  margin-left: 6px;
+  margin-right: 6px;
+}
+</style>

+ 358 - 0
src/pages/TestCase/components/test_case_list.vue

@@ -0,0 +1,358 @@
+<template>
+  <div>
+    <el-table
+      ref="testCaseTable"
+      v-loading="listLoading"
+      :data="testCaseDatas"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%"
+    >
+      <el-table-column type="expand" align="center" min-width="1%">
+        <template slot-scope="{row, $index}">
+          <defect-list :defects="row.defects" :selected-task-code="selectedTaskCode" ref="innerDefectList" :isContained="true" :test-case-can-edit="testCaseCanEdit"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="编号" prop="code" sortable="custom" align="center" min-width="3%">
+        <template slot-scope="{row}">
+          <span>{{ row.code }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="用例名称" align="center" :show-overflow-tooltip="true" min-width="6%">
+        <template slot-scope="{row}">
+          <span>{{ row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="优先级" align="center" min-width="3%">
+        <template slot-scope="{row}">
+          <span>{{ toPriorityCn(row.priority) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="描述" align="center" :show-overflow-tooltip="true" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.descr }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关联需求" align="center" :show-overflow-tooltip="true" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.demand }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.expectedResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.testResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结论" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <span>{{ toTestStatusCn(row.testStatus) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="缺陷数量(个)" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <a href="javascript:void(0)" @click="toogleExpand2(row)" style="color: indianred;">{{ row.defects.length }}</a>
+        </template>
+      </el-table-column>
+      <el-table-column label="审核结果" align="center" min-width="4%" v-if="!testCaseCanEdit">
+        <template slot-scope="{row}">
+          <span>{{ toExamStatusCn(row.examStatus) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="审核结果说明" align="center" :show-overflow-tooltip="true" min-width="8%"  v-if="!testCaseCanEdit">
+        <template slot-scope="{row}">
+          <span>{{ row.examDescr }}</span>
+        </template>
+      </el-table-column>
+      <!--      <el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">-->
+      <!--        <template slot-scope="{row}">-->
+      <!--          <span style="color:red;">{{ row.reviewer }}</span>-->
+      <!--        </template>-->
+      <!--      </el-table-column>-->
+      <!--      <el-table-column label="Imp" width="80px">-->
+      <!--        <template slot-scope="{row}">-->
+      <!--          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />-->
+      <!--        </template>-->
+      <!--      </el-table-column>-->
+      <el-table-column label="操作" align="center" min-width="5%">
+        <template slot-scope="{row,$index}">
+          <i class="el-icon-tickets mini-margin" @click="handleDetail(row)" v-if="!testCaseCanEdit && !canAudit" style="cursor: pointer;" title="查看详情"></i>
+          <i class="el-icon-edit mini-margin" @click="handleUpdate(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
+          <i class="el-icon-delete mini-margin" @click="handleDelete(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
+          <i class="el-icon-document-copy mini-margin" @click="handleCopy(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="复制"></i>
+          <i class="el-icon-document-add mini-margin" @click="handleCreateDefect(row, $index)" v-if="testCaseCanEdit && row.testStatus === 'NO_PASS'" style="cursor: pointer;" title="新增缺陷"></i>
+          <i class="el-icon-edit mini-margin" @click="handleDetail(row)" style="cursor: pointer;" title="审核"  v-if="canAudit"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination v-show="total>0" :total="total" :page.sync="listQueryParam.pageNo" :limit.sync="listQueryParam.pageSize" @pagination="getList" />
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
+      <testcase-form ref="testCaseForm" style="width: 600px;" :test-case-data="testCaseData" :read-only="!testCaseCanEdit"></testcase-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submit()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="用例详情" :visible.sync="detailDialogFormVisible" :close-on-click-modal="false">
+      <testcase-detail ref="testCaseDetail" style="width: 600px;" :test-case-data="testCaseData" :can-audit="canAudit"></testcase-detail>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submitAudit()" v-if="canAudit">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Pagination from '@/components/Pagination' // secondary package based on el-pagination
+import TestcaseForm from './testcase_form'
+import TestcaseDetail from './testcase_detail'
+import DefectList from './defect_list'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+import TestCaseUtils from '../utils'
+import {deepClone} from '@/utils/datas'
+import {mapGetters} from 'vuex'
+
+export default {
+  name: 'TestCaseList',
+  components: { Pagination, TestcaseForm, TestcaseDetail, DefectList },
+  data: function () {
+    return {
+      total: 0,
+      listQueryParam: {
+        pageNo: 1,
+        pageSize: 20,
+        selectedTaskCode: this.selectedTaskCode,
+        testStatus: this.selectedTestStatus,
+        examStatus: this.selectedExamStatus,
+        selectedUserId: this.selectedUserId
+      },
+      listLoading: false,
+      testCaseData: {},
+      testCaseDatas: [],
+      dialogFormVisible: false,
+      detailDialogFormVisible: false,
+      dialogStatus: '',
+      textMap: {
+        update: '编辑测试用例',
+        create: '新建测试用例'
+      }
+    }
+  },
+  props: {
+    selectedTaskCode: {
+      type: String,
+      required: true
+    },
+    selectedTestStatus: {
+      type: String,
+      default: ''
+    },
+    selectedExamStatus: {
+      type: String,
+      default: ''
+    },
+    selectedUserId: {
+      type: Number,
+      default: 0
+    },
+    testCaseCanEdit: {
+      type: Boolean,
+      default: false
+    },
+    canAudit: {
+      type: Boolean,
+      default: false
+    }
+  },
+  created () {
+    this.initData()
+    this.getList()
+  },
+  methods: {
+    ...TestCaseUtils,
+    ...mapGetters(['getRefreshTestCaseListFunc', 'getRefreshDefectListFunc']),
+    getList () {
+      this.showListLoading()
+      let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
+        .replace('{designerId}', this.listQueryParam.selectedUserId)
+        .replace('{pageNo}', this.listQueryParam.pageNo - 1)
+        .replace('{pageSize}', this.listQueryParam.pageSize)
+      if (this.listQueryParam.testStatus || this.listQueryParam.examStatus) {
+        if (url.indexOf('?') === -1) {
+          url = url + '?'
+        }
+        if (this.listQueryParam.testStatus) {
+          url = url + 'testStatus=' + this.listQueryParam.testStatus + '&'
+        }
+        if (this.listQueryParam.examStatus) {
+          url = url + 'examStatus=' + this.listQueryParam.examStatus + '&'
+        }
+        url = url.substring(0, url.length - 1)
+      }
+      Http.get(url).then((res) => {
+        const testCasePage = res.data
+        this.total = testCasePage.totalCount
+        this.listQueryParam.pageNo = testCasePage.pageNo + 1
+        this.listQueryParam.pageSize = testCasePage.pageSize
+        this.testCaseDatas = testCasePage.datas
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取测试用例数据失败:系统异常')
+      })
+      this.hideListLoading()
+    },
+    clearList () {
+      this.testCaseDatas.splice(0, this.testCaseDatas.length)
+      this.total = 0
+    },
+    initData () {
+      this.testCaseData = {
+        demand: '',
+        descr: '',
+        envConfig: '',
+        evaCriteria: '',
+        testStatus: '',
+        expectedResult: '',
+        files: [],
+        id: undefined,
+        designerId: this.selectedUserId,
+        inputDatas: '',
+        name: '',
+        code: '',
+        opeSteps: '',
+        others: '',
+        preconditions: '',
+        priority: '',
+        screenshots: [],
+        taskCode: this.selectedTaskCode,
+        testResult: '',
+        examStatus: '',
+        examDescr: '',
+        associatedCode: ''
+      }
+    },
+    handleCreate () {
+      this.initData()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['testCaseForm'].clearValidate()
+      })
+    },
+    handleCreateDefect (row, index) {
+      this.$refs.testCaseTable.toggleRowExpansion(row, true)
+      this.$nextTick(() => {
+        this.$refs.innerDefectList.handleCreate(row)
+      })
+    },
+    submit () {
+      let callback = null
+      if (this.testCaseData.id) {
+        callback = this.getList
+      } else {
+        callback = () => {
+          this.listQueryParam.pageNo = Math.ceil((this.total + 1) / this.listQueryParam.pageSize)
+          this.getList()
+        }
+      }
+      this.$refs.testCaseForm.submitForm(callback)
+    },
+    handleDetail (row) {
+      this.detailDialogFormVisible = true
+      deepClone(this.testCaseData, row)
+    },
+    handleUpdate (row) {
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      deepClone(this.testCaseData, row)
+      this.$nextTick(() => {
+        this.$refs['testCaseForm'].clearValidate()
+      })
+    },
+    handleDelete (row, index) {
+      this.$confirm('您确定要删除该条数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.delete(Api.TESTCASE.DELETE.replace('{id}', row.id), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            notify('success', '删除成功')
+            this.getList()
+          } else {
+            notify('error', '删除失败:' + res.data)
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '删除失败:系统异常')
+        })
+      }).catch(() => {
+      })
+    },
+    handleCopy (row) {
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      deepClone(this.testCaseData, row)
+      this.testCaseData.id = undefined
+      this.$nextTick(() => {
+        this.$refs['testCaseForm'].clearValidate()
+      })
+      console.log(this.testCaseData)
+    },
+    submitAudit () {
+      this.$refs.testCaseDetail.submitAuditResult((examStatus, examDescr) => {
+        let testCase = this.testCaseDatas.find(testCase => testCase.id === this.testCaseData.id)
+        testCase.examStatus = examStatus
+        testCase.examDescr = examDescr
+      })
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    toogleExpand2 (row) {
+      let testCaseTable = this.$refs.testCaseTable
+      testCaseTable.toggleRowExpansion(row)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .mini-margin {
+    margin-left: 6px;
+    margin-right: 6px;
+  }
+</style>
+<style>
+  .el-table__expanded-cell {
+    background-color: #eee!important;
+  }
+  .el-table__expanded-cell:hover {
+    background-color: #eee!important;
+  }
+</style>

+ 298 - 0
src/pages/TestCase/components/test_env_list.vue

@@ -0,0 +1,298 @@
+<template>
+  <div>
+    <el-table
+      :data="testEnvs"
+      border
+      fit
+      style="width: 100%"
+      v-loading="listLoading"
+    >
+      <el-table-column label="序号" align="center" min-width="7%">
+        <template slot-scope="{row, $index}">
+          <span>{{ $index + 1 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="设备名称" align="center" :show-overflow-tooltip="true" min-width="25%">
+        <template slot-scope="{row}">
+          <span>{{ row.devName }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="设备型号" align="center" min-width="25%">
+        <template slot-scope="{row}">
+          <span>{{ row.devModel }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="数量" align="center" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.devCount }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="硬件配置" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.devConfig }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作系统" align="center" min-width="20%">
+        <template slot-scope="{row}">
+          <span>{{ row.opeSys }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="中间件" align="center" min-width="20%">
+        <template slot-scope="{row}">
+          <span>{{ row.middleware }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="数据库" align="center" min-width="20%">
+        <template slot-scope="{row}">
+          <span>{{ row.db }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="浏览器" align="center" min-width="20%">
+        <template slot-scope="{row}">
+          <span>{{ row.browser }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="支持软件" align="center" min-width="25%">
+        <template slot-scope="{row}">
+          <span>{{ row.supportSoftware }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
+        <template slot-scope="{row,$index}">
+          <i class="el-icon-edit mini-margin " @click="handleUpdate(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
+          <i class="el-icon-delete mini-margin " @click="handleDelete(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
+      <el-form ref="testEnvForm" :rules="rules" :model="testEnvData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="任务编号" prop="taskCode">
+          <el-input v-model="testEnvData.taskCode" disabled/>
+        </el-form-item>
+        <el-form-item label="设备名字" prop="devName">
+          <el-input v-model="testEnvData.devName" :autosize="{ minRows: 1}"/>
+        </el-form-item>
+        <el-form-item label="设备型号" prop="devModel">
+          <el-input v-model="testEnvData.devModel" :autosize="{ minRows: 1}"/>
+        </el-form-item>
+        <el-form-item label="设备数量" prop="devCount">
+          <el-input v-model="testEnvData.devCount"/>
+        </el-form-item>
+        <el-form-item label="硬件配置" prop="devConfig">
+          <el-input type="textarea" rows="5" v-model="testEnvData.devConfig" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+        <el-form-item label="操作系统" prop="opeSys">
+          <el-input v-model="testEnvData.opeSys" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+        <el-form-item label="中间件" prop="middleware">
+          <el-input v-model="testEnvData.middleware" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+        <el-form-item label="数据库" prop="db">
+          <el-input v-model="testEnvData.db" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+        <el-form-item label="浏览器" prop="browser">
+          <el-input v-model="testEnvData.browser" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+        <el-form-item label="支持软件" prop="supportSoftware">
+          <el-input v-model="testEnvData.supportSoftware" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submit()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {deepClone} from '@/utils/datas'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+
+export default {
+  name: 'TestEnvList',
+  data: function () {
+    return {
+      dialogFormVisible: false,
+      textMap: {
+        create: '新建测试环境',
+        update: '编辑测试环境'
+      },
+      testEnvData: {},
+      listLoading: false,
+      loading: false,
+      dialogStatus: '',
+      listQueryParam: {
+        selectedTaskCode: this.selectedTaskCode,
+        selectedUserId: this.selectedUserId
+      },
+      testEnvs: [],
+      rules: {
+        devName: [
+          {required: true, message: '设备名称不可为空', trigger: 'blur'}
+        ],
+        devCount: [
+          {required: true, message: '设备数量不可为空', trigger: 'blur'}
+        ],
+        devConfig: [
+          {required: true, message: '硬件配置不可为空', trigger: 'blur'}
+        ],
+        opeSys: [
+          {required: true, message: '操作系统不可为空', trigger: 'blur'}
+        ]
+      }
+    }
+  },
+  props: {
+    'testCaseCanEdit': {
+      type: Boolean,
+      default: false
+    },
+    selectedTaskCode: {
+      type: String,
+      default: ''
+    },
+    selectedUserId: {
+      type: Number,
+      default: 0
+    }
+  },
+  created () {
+    this.initTestEnvData()
+    this.getList()
+  },
+  methods: {
+    handleCreate () {
+      this.initTestEnvData()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs.testEnvForm.clearValidate()
+      })
+    },
+    handleUpdate (row) {
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      deepClone(this.testEnvData, row)
+      this.$nextTick(() => {
+        this.$refs.testEnvForm.clearValidate()
+      })
+    },
+    handleDelete (row, index) {
+      this.$confirm('您确定要删除该条数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.delete(Api.TESTENV.DELETE.replace('{id}', row.id), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            notify('success', '删除成功')
+            this.getList()
+          } else {
+            notify('error', '删除失败:' + res.data)
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '删除失败:系统异常')
+        })
+      }).catch(() => {
+      })
+    },
+    submit () {
+      this.$refs.testEnvForm.validate(valid => {
+        if (valid) {
+          this.showLoading()
+          let url
+          let submitDataMethod
+          if (this.testEnvData.id) {
+            url = Api.TESTENV.UPDATE.replace('{id}', this.testEnvData.id)
+            submitDataMethod = Http.put
+          } else {
+            url = Api.TESTENV.ADD
+            submitDataMethod = Http.post
+          }
+          submitDataMethod(url, this.testEnvData).then((res) => {
+            console.log(res)
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交测试环境失败:' + res.data)
+            } else {
+              if (!this.testEnvData.id) {
+                this.testEnvData.id = res.data
+              }
+              notify('success', '提交成功')
+              this.getList()
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '测试环境创建失败:系统异常')
+          })
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    hideLoading () {
+      this.loading = false
+    },
+    showLoading () {
+      this.loading = true
+    },
+    initTestEnvData () {
+      this.testEnvData = {
+        id: undefined,
+        taskCode: this.listQueryParam.selectedTaskCode,
+        devName: '',
+        devModel: '',
+        devCount: undefined,
+        devConfig: 'CPU:\n内存:\n硬盘:\n其他:',
+        opeSys: '',
+        middleware: '',
+        db: '',
+        browser: '',
+        supportSoftware: ''
+      }
+    },
+    getList () {
+      this.showListLoading()
+      let url = Api.TESTENV.LIST.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
+        .replace('{designerId}', this.listQueryParam.selectedUserId)
+      Http.get(url).then((res) => {
+        this.testEnvs = res.data
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取测试环境数据失败:系统异常')
+      })
+      this.hideListLoading()
+    },
+    clearList () {
+      this.testEnvs.splice(0, this.testEnvs.length)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .mini-margin {
+    margin-left: 6px;
+    margin-right: 6px;
+  }
+</style>

+ 244 - 0
src/pages/TestCase/components/test_tool_list.vue

@@ -0,0 +1,244 @@
+<template>
+  <div>
+    <el-table
+      :data="testTools"
+      border
+      fit
+      style="width: 100%"
+      v-loading="listLoading"
+    >
+      <el-table-column label="序号" align="center" min-width="15%">
+        <template slot-scope="{row, $index}">
+          <span>{{ $index + 1 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="工具名称" align="center" :show-overflow-tooltip="true" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="工具版本" align="center" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.version }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="生产商/来源" align="center" min-width="35%">
+        <template slot-scope="{row}">
+          <span>{{ row.producerFrom }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="用途" align="center" min-width="70%">
+        <template slot-scope="{row}">
+          <span>{{ row.purpose }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
+        <template slot-scope="{row,$index}">
+          <i class="el-icon-edit mini-margin " @click="handleUpdate(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
+          <i class="el-icon-delete mini-margin " @click="handleDelete(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
+      <el-form ref="testToolForm" :rules="rules" :model="testToolData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="任务编号" prop="taskCode">
+          <el-input v-model="testToolData.taskCode" disabled/>
+        </el-form-item>
+        <el-form-item label="工具名字" prop="name">
+          <el-input v-model="testToolData.name" :autosize="{ minRows:1}"/>
+        </el-form-item>
+        <el-form-item label="工具版本" prop="version">
+          <el-input v-model="testToolData.version" :autosize="{ minRows: 1}"/>
+        </el-form-item>
+        <el-form-item label="生产商/来源" prop="producerFrom">
+          <el-input v-model="testToolData.producerFrom" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+        <el-form-item label="用途" prop="purpose">
+          <el-input type="textarea" v-model="testToolData.purpose" :autosize="{ minRows: 2}"/>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submit()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {deepClone} from '@/utils/datas'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+
+export default {
+  name: 'TestToolList',
+  data: function () {
+    return {
+      dialogFormVisible: false,
+      textMap: {
+        create: '新建测试环境',
+        update: '编辑测试环境'
+      },
+      testToolData: {},
+      listLoading: false,
+      loading: false,
+      dialogStatus: '',
+      listQueryParam: {
+        selectedTaskCode: this.selectedTaskCode,
+        selectedUserId: this.selectedUserId
+      },
+      testTools: [],
+      rules: {
+        name: [
+          {required: true, message: '工具名字不可为空', trigger: 'blur'}
+        ]
+      }
+    }
+  },
+  props: {
+    'testCaseCanEdit': {
+      type: Boolean,
+      default: false
+    },
+    selectedTaskCode: {
+      type: String,
+      default: ''
+    },
+    selectedUserId: {
+      type: Number,
+      default: 0
+    }
+  },
+  created () {
+    this.initTestToolData()
+    this.getList()
+  },
+  methods: {
+    handleCreate () {
+      this.initTestToolData()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs.testToolForm.clearValidate()
+      })
+    },
+    handleUpdate (row) {
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      deepClone(this.testToolData, row)
+      this.$nextTick(() => {
+        this.$refs.testToolForm.clearValidate()
+      })
+    },
+    handleDelete (row, index) {
+      this.$confirm('您确定要删除该条数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.delete(Api.TESTTOOL.DELETE.replace('{id}', row.id), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            notify('success', '删除成功')
+            this.getList()
+          } else {
+            notify('error', '删除失败:' + res.data)
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '删除失败:系统异常')
+        })
+      }).catch(() => {
+      })
+    },
+    submit () {
+      this.$refs.testToolForm.validate(valid => {
+        if (valid) {
+          this.showLoading()
+          let url
+          let submitDataMethod
+          if (this.testToolData.id) {
+            url = Api.TESTTOOL.UPDATE.replace('{id}', this.testToolData.id)
+            submitDataMethod = Http.put
+          } else {
+            url = Api.TESTTOOL.ADD
+            submitDataMethod = Http.post
+          }
+          submitDataMethod(url, this.testToolData).then((res) => {
+            console.log(res)
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交测试环境失败:' + res.data)
+            } else {
+              if (!this.testToolData.id) {
+                this.testToolData.id = res.data
+              }
+              notify('success', '提交成功')
+              this.getList()
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '测试环境创建失败:系统异常')
+          })
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    hideLoading () {
+      this.loading = false
+    },
+    showLoading () {
+      this.loading = true
+    },
+    initTestToolData () {
+      this.testToolData = {
+        id: undefined,
+        taskCode: this.listQueryParam.selectedTaskCode,
+        name: '',
+        version: '',
+        producerFrom: undefined,
+        purpose: ''
+      }
+    },
+    getList () {
+      this.showListLoading()
+      let url = Api.TESTTOOL.LIST.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
+        .replace('{designerId}', this.listQueryParam.selectedUserId)
+      Http.get(url).then((res) => {
+        this.testTools = res.data
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取测试工具数据失败:系统异常')
+      })
+      this.hideListLoading()
+    },
+    clearList () {
+      this.testTools.splice(0, this.testTools.length)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .mini-margin {
+    margin-left: 6px;
+    margin-right: 6px;
+  }
+</style>

+ 6 - 2
src/pages/TestCase/components/testcase_detail.vue

@@ -6,6 +6,9 @@
     <el-form-item label="用例编号" prop="code">
       <span>{{testCaseData.code}}</span>
     </el-form-item>
+    <el-form-item label="关联用例" prop="code">
+      <span>{{testCaseData.associatedCode}}</span>
+    </el-form-item>
     <el-form-item label="用例名字" prop="name">
       <span>{{testCaseData.name}}</span>
     </el-form-item>
@@ -73,7 +76,7 @@
       </el-select>
     </el-form-item>
     <el-form-item label="审核结果说明" prop="examDescr" v-if="canAudit">
-      <el-input type="textarea" v-model="testCaseData.examDescr"/>
+      <el-input type="textarea" v-model="testCaseData.examDescr" :autosize="{ minRows: 2}"/>
     </el-form-item>
   </el-form>
 </template>
@@ -126,7 +129,8 @@ export default {
           files: [],
           screenshots: [],
           examStatus: '',
-          examDescr: ''
+          examDescr: '',
+          associatedCode: ''
         }
       }
     }

+ 128 - 68
src/pages/TestCase/components/testcase_form.vue

@@ -1,70 +1,80 @@
 <template>
-  <el-form ref="testCaseForm" :rules="rules" :model="testCaseData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+  <el-form ref="testCaseForm" :rules="rules" :model="testCase" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
     <el-form-item label="任务编号" prop="taskCode">
-      <el-input v-model="testCaseData.taskCode" disabled/>
+      <el-input v-model="testCase.taskCode" disabled/>
     </el-form-item>
     <el-form-item label="用例名字" prop="name">
-      <el-input v-model="testCaseData.name" :disabled="readOnly"/>
+      <el-input v-model="testCase.name" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="关联用例" prop="associatedCode">
+      <el-select v-model="testCase.associatedCode" filterable :filter-method="dataFilter" @click.native="eqNoClick" :disabled="readOnly" :clearable="true">
+        <el-option
+          v-for="testCase in searchTestCases"
+          :key="testCase.code"
+          :label="testCase.name"
+          :value="testCase.code"
+        />
+      </el-select>
     </el-form-item>
     <el-form-item label="用例描述" prop="descr">
-      <el-input type="textarea" v-model="testCaseData.descr" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.descr" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="关联需求" prop="demand">
-      <el-input type="textarea" v-model="testCaseData.demand" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.demand" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="优先级" prop="priority">
-      <el-select v-model="testCaseData.priority" :disabled="readOnly">
+      <el-select v-model="testCase.priority" :disabled="readOnly">
         <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="preconditions">
-      <el-input type="textarea" v-model="testCaseData.preconditions" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.preconditions" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="环境配置" prop="envConfig">
-      <el-input type="textarea" v-model="testCaseData.envConfig" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.envConfig" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="操作步骤" prop="opeSteps">
-      <el-input type="textarea" v-model="testCaseData.opeSteps" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.opeSteps" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="输入数据" prop="inputDatas">
-      <el-input type="textarea" v-model="testCaseData.inputDatas" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.inputDatas" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="预期结果" prop="expectedResult">
-      <el-input type="textarea" v-model="testCaseData.expectedResult" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.expectedResult" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="评判标准" prop="evaCriteria">
-      <el-input type="textarea" v-model="testCaseData.evaCriteria" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.evaCriteria" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="其他说明" prop="others">
-      <el-input type="textarea" v-model="testCaseData.others" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.others" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="测试结果" prop="testResult">
-      <el-input type="textarea" v-model="testCaseData.testResult" :disabled="readOnly"/>
+      <el-input type="textarea" v-model="testCase.testResult" :disabled="readOnly" :autosize="{ minRows: 2}"/>
     </el-form-item>
     <el-form-item label="测试结论" prop="testStatus">
-      <el-select v-model="testCaseData.testStatus" :disabled="readOnly">
+      <el-select v-model="testCase.testStatus" :disabled="readOnly">
         <el-option value="WAIT" label="待测试"/>
         <el-option value="PASS" label="通过"/>
         <el-option value="NO_PASS" label="不通过"/>
       </el-select>
     </el-form-item>
     <el-form-item label="附件" prop="files">
-      <file-upload :countLimit="countLimit" :files="testCaseData.files" :disabled="readOnly"></file-upload>
+      <file-upload :countLimit="countLimit" :files="testCase.files" :disabled="readOnly"></file-upload>
     </el-form-item>
     <el-form-item label="截图" prop="screenshots">
-      <img-upload :countLimit="countLimit" :files="testCaseData.screenshots" :disabled="readOnly"></img-upload>
+      <img-upload :countLimit="countLimit" :files="testCase.screenshots" :disabled="readOnly"></img-upload>
     </el-form-item>
     <el-form-item label="审核结果" prop="examStatus" v-if="isCommitted">
-      <el-select v-model="testCaseData.examStatus" :disabled="canAudit">
+      <el-select v-model="testCase.examStatus" :disabled="canAudit">
         <el-option value="WAIT" label="待审核"/>
         <el-option value="PASS" label="有效"/>
         <el-option value="NO_PASS" label="无效"/>
       </el-select>
     </el-form-item>
     <el-form-item label="审核结果说明" prop="testResult" v-if="isCommitted">
-      <el-input type="textarea" v-model="testCaseData.examDescr" :disabled="canAudit"/>
+      <el-input type="textarea" v-model="testCase.examDescr" :disabled="canAudit" :autosize="{ minRows: 2}"/>
     </el-form-item>
   </el-form>
 </template>
@@ -76,6 +86,7 @@ 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: 'TestcaseForm',
@@ -89,13 +100,16 @@ export default {
           {min: 1, max: 50, message: '用例名称长度在 1 到 50 个字符', trigger: 'blur'}
         ],
         priority: [
-          {required: true, message: '优先级不可为空'}
+          {required: true, message: '优先级不可为空', trigger: 'blur'}
         ],
         testStatus: [
-          {required: true, message: '测试结论不可为空'}
+          {required: true, message: '测试结论不可为空', trigger: 'blur'}
         ]
       },
-      loading: false
+      loading: false,
+      testCases: [],
+      searchTestCases: [],
+      testCase: this.init()
     }
   },
   props: {
@@ -113,58 +127,88 @@ export default {
     },
     testCaseData: {
       type: Object,
-      default: function () {
-        return {
-          id: undefined,
-          taskCode: '',
-          name: '',
-          descr: '',
-          demand: '',
-          preconditions: '',
-          envConfig: '',
-          priority: undefined,
-          opeSteps: '',
-          inputDatas: '',
-          expectedResult: '',
-          evaCriteria: '',
-          others: '',
-          testResult: '',
-          testStatus: undefined,
-          files: [],
-          screenshots: [],
-          examStatus: '',
-          examDescr: ''
-        }
-      }
+      required: true,
+      default: this.init
+    }
+  },
+  watch: {
+    testCaseData: {
+      immediate: true,
+      handler (nv, ov) {
+        deepClone(this.testCase, nv)
+      },
+      deep: true
     }
   },
   methods: {
+    init () {
+      return {
+        id: undefined,
+        code: '',
+        taskCode: '',
+        designerId: undefined,
+        name: '',
+        descr: '',
+        demand: '',
+        preconditions: '',
+        envConfig: '',
+        priority: undefined,
+        opeSteps: '',
+        inputDatas: '',
+        expectedResult: '',
+        evaCriteria: '',
+        others: '',
+        testResult: '',
+        testStatus: undefined,
+        files: [],
+        screenshots: [],
+        examStatus: '',
+        examDescr: '',
+        associatedCode: ''
+      }
+    },
+    getList () {
+      let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.testCase.taskCode)
+        .replace('{designerId}', this.testCase.designerId)
+        .replace('{pageNo}', 0)
+        .replace('{pageSize}', 1000)
+      Http.get(url).then((res) => {
+        const testCasePage = res.data
+        this.testCases = testCasePage.datas.filter(testCase => {
+          return testCase.name !== this.testCase.name
+        })
+        this.searchTestCases = this.testCases
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取测试用例数据失败:系统异常')
+      })
+    },
     submitForm (callback) {
       this.$refs['testCaseForm'].validate(valid => {
         if (valid) {
           this.showLoading()
           const newTestCase = {
-            taskCode: this.testCaseData.taskCode,
-            name: this.testCaseData.name,
-            descr: this.testCaseData.descr,
-            demand: this.testCaseData.demand,
-            preconditions: this.testCaseData.preconditions,
-            envConfig: this.testCaseData.envConfig,
-            priority: this.testCaseData.priority,
-            opeSteps: this.testCaseData.opeSteps,
-            inputDatas: this.testCaseData.inputDatas,
-            expectedResult: this.testCaseData.expectedResult,
-            evaCriteria: this.testCaseData.evaCriteria,
-            others: this.testCaseData.others,
-            testResult: this.testCaseData.testResult,
-            testStatus: this.testCaseData.testStatus,
-            files: this.testCaseData.files,
-            screenshots: this.testCaseData.screenshots
+            taskCode: this.testCase.taskCode,
+            name: this.testCase.name,
+            descr: this.testCase.descr,
+            demand: this.testCase.demand,
+            preconditions: this.testCase.preconditions,
+            envConfig: this.testCase.envConfig,
+            priority: this.testCase.priority,
+            opeSteps: this.testCase.opeSteps,
+            inputDatas: this.testCase.inputDatas,
+            expectedResult: this.testCase.expectedResult,
+            evaCriteria: this.testCase.evaCriteria,
+            others: this.testCase.others,
+            testResult: this.testCase.testResult,
+            testStatus: this.testCase.testStatus,
+            files: this.testCase.files,
+            screenshots: this.testCase.screenshots,
+            associatedCode: this.testCase.associatedCode
           }
-          if (this.testCaseData.id && this.testCaseData.id > 0) {
-            newTestCase['id'] = this.testCaseData.id
+          if (this.testCase.id && this.testCase.id > 0) {
+            newTestCase['id'] = this.testCase.id
           }
-          console.log(newTestCase)
           let url
           let submitDataMethod
           if (newTestCase['id']) {
@@ -180,8 +224,11 @@ export default {
             if (res.code !== 20000) {
               notify('error', '提交测试用例失败:' + res.data)
             } else {
-              this.testCaseData.id = res.data
               notify('success', '提交成功')
+              if (!this.testCase.id) {
+                deepClone(this.testCase, this.testCaseData)
+                this.clearValidate()
+              }
               callback()
             }
           }).catch((error) => {
@@ -200,9 +247,9 @@ export default {
         if (valid) {
           this.showLoading()
           const auditResult = {
-            id: this.testCaseData.id,
-            examStatus: this.testCaseData.examStatus,
-            examDescr: this.testCaseData.examDescr
+            id: this.testCase.id,
+            examStatus: this.testCase.examStatus,
+            examDescr: this.testCase.examDescr
           }
           Http.put(url, auditResult).then((res) => {
             console.log(res)
@@ -232,6 +279,19 @@ export default {
     },
     clearValidate (props = []) {
       this.$refs['testCaseForm'].clearValidate()
+      this.getList()
+    },
+    dataFilter (val) {
+      if (val) {
+        this.searchTestCases = this.testCases.filter(testCase => {
+          return testCase.name.includes(val)
+        })
+      } else {
+        this.searchTestCases = this.testCases
+      }
+    },
+    eqNoClick () {
+      this.searchTestCases = this.testCases
     }
   }
 }

+ 114 - 378
src/pages/TestCase/exam_testcases.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="app-container">
-    <div class="title h1" style="margin-top: 10px;">测试用例审核</div>
+    <div class="title h1" style="margin-top: 10px;">测审核</div>
     <div class="filter-container">
-      <el-select v-model="listQueryParam.selectedProjectCode"  filterable :filter-method="projectDataFilter" @click.native="eqNoClick">
+      <el-select v-model="selectedProjectCode"  filterable :filter-method="projectDataFilter" @click.native="eqNoClick">
         <el-option
           v-for="project in searchProjects"
           :key="project.code"
@@ -10,7 +10,7 @@
           :value="project.code"
         />
       </el-select>
-      <el-select v-model="listQueryParam.selectedTaskCode"  filterable :filter-method="taskDataFilter" @click.native="eqNoClick">
+      <el-select v-model="selectedTaskCode"  filterable :filter-method="taskDataFilter" @click.native="eqNoClick">
         <el-option
           v-for="task in searchTasks"
           :key="task.code"
@@ -18,7 +18,7 @@
           :value="task.code"
         />
       </el-select>
-      <el-select v-model="listQueryParam.selectedUserId">
+      <el-select v-model="selectedUserId">
         <el-option
           v-for="user in users"
           :key="'user' + user.id"
@@ -26,225 +26,59 @@
           :value="user.id"
         />
       </el-select>
-      <el-select v-model="listQueryParam.testStatus">
+      <el-select v-if="testStatusSearchShow" v-model="selectedTestStatus">
         <el-option value="" label="---测试结论---"/>
         <el-option value="WAIT" label="待测试"/>
         <el-option value="PASS" label="通过"/>
         <el-option value="NO_PASS" label="不通过"/>
       </el-select>
-      <el-select v-model="listQueryParam.examStatus">
+      <el-select v-if="examStatusSearchShow" v-model="selectedExamStatus">
         <el-option value="" label="---审核结果---"/>
         <el-option value="WAIT" label="待审核"/>
         <el-option value="VALID" label="有效"/>
         <el-option value="INVALID" label="无效"/>
       </el-select>
     </div>
-
-    <el-table
-      ref="testCaseTable"
-      :key="tableKey"
-      v-loading="listLoading"
-      :data="testCaseDatas"
-      border
-      fit
-      highlight-current-row
-      style="width: 100%"
-    >
-      <el-table-column type="expand" align="center"  min-width="1%">
-        <template slot-scope="{row}">
-          <el-table
-            :key="'innerTableKey' + row.id"
-            :data="row.defects"
-            border
-            fit
-            style="width: 100%"
-          >
-            <el-table-column label="编号" align="center" min-width="15%">
-              <template slot-scope="{row}">
-                <span>{{ row.code }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="缺陷描述" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.descr }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="严重等级" align="center" min-width="12%">
-              <template slot-scope="{row}">
-                <span>{{ toSeriousnessCn(row.seriousness) }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="优先级" align="center" min-width="10%">
-              <template slot-scope="{row}">
-                <span>{{ toPriorityCn(row.priority) }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="缺陷类型" align="center" min-width="12%">
-              <template slot-scope="{row}">
-                <span>{{ toDefectTypeCn(row.defectType) }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作步骤" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.opeSteps }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="输入数据" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.inputDatas }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.expectedResult }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.testResult }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
-              <template slot-scope="{row,$index}">
-                <i class="el-icon-tickets mini-margin" @click="handleDefectDetail(row)" style="cursor: pointer;" title="查看详情"></i>
-              </template>
-            </el-table-column>
-          </el-table>
-        </template>
-      </el-table-column>
-      <el-table-column label="编号" prop="code" sortable="custom" align="center" min-width="3%">
-        <template slot-scope="{row}">
-          <span>{{ row.code }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="用例名称" align="center" :show-overflow-tooltip="true" min-width="6%">
-        <template slot-scope="{row}">
-          <span>{{ row.name }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="优先级" align="center" min-width="3%">
-        <template slot-scope="{row}">
-          <span>{{ toPriorityCn(row.priority) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="描述" align="center" :show-overflow-tooltip="true" min-width="10%">
-        <template slot-scope="{row}">
-          <span>{{ row.descr }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="关联需求" align="center" :show-overflow-tooltip="true" min-width="10%">
-        <template slot-scope="{row}">
-          <span>{{ row.demand }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="8%">
-        <template slot-scope="{row}">
-          <span>{{ row.expectedResult }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="8%">
-        <template slot-scope="{row}">
-          <span>{{ row.testResult }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="测试结论" align="center" min-width="4%">
-        <template slot-scope="{row}">
-          <span>{{ toTestStatusCn(row.testStatus) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="缺陷数量" align="center" min-width="4%">
-        <template slot-scope="{row}">
-          <a href="javascript:void(0)" @click="toogleExpand2(row)" style="color: red;">{{ row.defects.length }}个</a>
-        </template>
-      </el-table-column>
-      <el-table-column label="审核结果" align="center" min-width="4%">
-        <template slot-scope="{row}">
-          <span>{{ toExamStatusCn(row.examStatus) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="审核结果说明" align="center" :show-overflow-tooltip="true" min-width="8%">
-        <template slot-scope="{row}">
-          <span>{{ row.examDescr }}</span>
-        </template>
-      </el-table-column>
-<!--      <el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">-->
-<!--        <template slot-scope="{row}">-->
-<!--          <span style="color:red;">{{ row.reviewer }}</span>-->
-<!--        </template>-->
-<!--      </el-table-column>-->
-<!--      <el-table-column label="Imp" width="80px">-->
-<!--        <template slot-scope="{row}">-->
-<!--          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />-->
-<!--        </template>-->
-<!--      </el-table-column>-->
-      <el-table-column label="操作" align="center" min-width="5%">
-        <template slot-scope="{row,$index}">
-          <i class="el-icon-tickets mini-margin" @click="handleDetail(row)" style="cursor: pointer;" title="查看详情" v-if="!canAudit"></i>
-          <i class="el-icon-edit mini-margin" @click="handleDetail(row)" style="cursor: pointer;" title="审核"  v-if="canAudit"></i>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination v-show="total>0" :total="total" :page.sync="listQueryParam.pageNo" :limit.sync="listQueryParam.pageSize" @pagination="getList" />
-
-    <el-dialog title="用例详情" :visible.sync="dialogFormVisible">
-      <testcase-detail ref="testCaseDetail" style="width: 600px;" :test-case-data="testCaseData" :can-audit="canAudit"></testcase-detail>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="dialogFormVisible = false">
-          取消
-        </el-button>
-        <el-button type="primary" @click="submitData()" v-if="canAudit">
-          提交
-        </el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog title="缺陷详情" :visible.sync="defectDialogFormVisible">
-      <defect-detail ref="defectDetail" style="width: 600px;" :defect-data="defectData"></defect-detail>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="defectDialogFormVisible = false">
-          取消
-        </el-button>
-      </div>
-    </el-dialog>
+    <el-tabs v-model="tabActiveName" @tab-click="handleTabClick">
+      <el-tab-pane label="测试用例" name="testCases">
+        <test-case-list ref="testCaseList" :selected-task-code="selectedTaskCode" :selected-user-id="selectedUserId" :selected-test-status="selectedTestStatus" :selected-exam-status="selectedExamStatus" :test-case-can-edit="false" :can-audit="canAudit"></test-case-list>
+      </el-tab-pane>
+      <el-tab-pane label="用例缺陷" name="defects">
+        <defect-list ref="defectList" :isContained="false" :test-case-can-edit="false" :selected-task-code="selectedTaskCode" :selected-user-id="selectedUserId"></defect-list>
+      </el-tab-pane>
+      <el-tab-pane label="测试环境" name="testEnvs">
+        <test-env-list ref="testEnvList" :test-case-can-edit="false" :selected-task-code="selectedTaskCode" :selected-user-id="selectedUserId"></test-env-list>
+      </el-tab-pane>
+      <el-tab-pane label="测试工具" name="testTools">
+        <test-tool-list ref="testToolList" :test-case-can-edit="false" :selected-task-code="selectedTaskCode" :selected-user-id="selectedUserId"></test-tool-list>
+      </el-tab-pane>
+    </el-tabs>
   </div>
 </template>
 
 <script>
-import waves from '@/directive/waves' // waves directive
-import Pagination from '@/components/Pagination' // secondary package based on el-pagination
-import TestcaseDetail from './components/testcase_detail'
-import DefectDetail from './components/defect_detail'
+import TestCaseList from './components/test_case_list'
+import DefectList from './components/defect_list'
+import TestEnvList from './components/test_env_list'
+import TestToolList from './components/test_tool_list'
 import Http from '@/js/http'
 import Api from '@/js/api'
 import {notify} from '@/constants'
-import TestCaseUtils from './utils'
 
 export default {
-  name: 'ComplexTable',
-  components: { Pagination, TestcaseDetail, DefectDetail },
-  directives: { waves },
+  name: 'ExamTestCases',
+  components: { TestCaseList, DefectList, TestEnvList, TestToolList },
   data () {
     return {
+      tabActiveName: 'testCases',
       testCaseData: {},
       defectData: {},
       testCaseDatas: [],
-      tableKey: 0,
-      total: 0,
-      listQueryParam: {
-        pageNo: 1,
-        pageSize: 20,
-        selectedProjectCode: this.$route.params.projectCode,
-        selectedTaskCode: this.$route.params.taskCode,
-        selectedUserId: parseInt(this.$route.params.userId),
-        testStatus: '',
-        examStatus: ''
-      },
-      listLoading: true,
-      showReviewer: false,
-      dialogFormVisible: false,
-      defectDialogFormVisible: false,
-      selectedTaskCode: '',
+      selectedProjectCode: this.$route.params.projectCode,
+      selectedTaskCode: this.$route.params.taskCode,
+      selectedUserId: parseInt(this.$route.params.userId),
+      selectedTestStatus: '',
+      selectedExamStatus: '',
       selectedTaskData: {
         name: '',
         code: '',
@@ -262,149 +96,83 @@ export default {
         isCommitted: undefined,
         id: undefined
       },
-      canAudit: false
+      canAudit: false,
+      testStatusSearchShow: true,
+      examStatusSearchShow: true
     }
   },
   watch: {
-    'listQueryParam.selectedProjectCode': {
+    'selectedProjectCode': {
       immediate: false,
       handler: function () {
-        this.getSimpleTaskDatas()
+        this.getSimpleTaskDatas(false)
       }
     },
-    'listQueryParam.selectedTaskCode': {
+    'selectedTaskCode': {
       immediate: false,
       handler: function () {
-        this.getTaskUserDatas()
+        this.getTaskUserDatas(false)
       }
     },
-    'listQueryParam.selectedUserId': {
+    'selectedUserId': {
       immediate: false,
       handler: function () {
-        this.getList()
+        if (this.selectedUserId) {
+          this.getList(false)
+        }
       }
     },
-    'listQueryParam.examStatus': {
+    'selectedExamStatus': {
       immediate: false,
       handler: function () {
-        this.getList()
+        if (this.selectedUserId) {
+          this.getList(false)
+        }
       }
     },
-    'listQueryParam.testStatus': {
+    'selectedTestStatus': {
       immediate: false,
       handler: function () {
-        this.getList()
+        if (this.selectedUserId) {
+          this.getList(false)
+        }
       }
     }
   },
   created () {
-    this.initData()
-    this.initDefectData()
     this.getSimpleProjectDatas()
-    this.getSimpleTaskDatas()
-    this.getTaskUserDatas()
+    this.getSimpleTaskDatas(true)
   },
   methods: {
-    ...TestCaseUtils,
-    getList () {
-      this.showListLoading()
-      let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
-        .replace('{designerId}', this.listQueryParam.selectedUserId)
-        .replace('{pageNo}', this.listQueryParam.pageNo - 1)
-        .replace('{pageSize}', this.listQueryParam.pageSize)
-      if (this.listQueryParam.testStatus || this.listQueryParam.examStatus) {
-        if (url.indexOf('?') === -1) {
-          url = url + '?'
-        }
-        if (this.listQueryParam.testStatus) {
-          url = url + 'testStatus=' + this.listQueryParam.testStatus + '&'
-        }
-        if (this.listQueryParam.examStatus) {
-          url = url + 'examStatus=' + this.listQueryParam.examStatus + '&'
+    getList (firstIn) {
+      this.canAudit = this.selectedTaskData.status !== 4 && this.selectedUser.isCommitted === 1
+      this.$refs.testCaseList.listQueryParam.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testCaseList.listQueryParam.selectedUserId = this.selectedUserId
+      this.$refs.testCaseList.listQueryParam.testStatus = this.selectedTestStatus
+      this.$refs.testCaseList.listQueryParam.examStatus = this.selectedExamStatus
+      this.$refs.defectList.listQueryParam.selectedTaskCode = this.selectedTaskCode
+      this.$refs.defectList.listQueryParam.selectedUserId = this.selectedUserId
+      this.$refs.testEnvList.listQueryParam.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testEnvList.listQueryParam.selectedUserId = this.selectedUserId
+      this.$refs.testToolList.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testToolList.selectedUserId = this.selectedUserId
+      // 非第一次进到这个页面
+      if (!firstIn) {
+        if (this.selectedUserId) {
+          this.$refs.testCaseList.getList()
+          this.$refs.defectList.getList()
+          this.$refs.testEnvList.getList()
+          this.$refs.testToolList.getList()
+        } else {
+          this.clearList()
         }
-        url = url.substring(0, url.length - 1)
       }
-      Http.get(url).then((res) => {
-        const testCasePage = res.data
-        this.total = testCasePage.totalCount
-        this.pageNo = testCasePage.pageNo + 1
-        this.pageLimit = testCasePage.pageSize
-        this.testCaseDatas = testCasePage.datas
-        this.selectedUser = this.users.find(user => user.id === this.listQueryParam.selectedUserId)
-        this.canAudit = this.selectedTaskData.status !== 4 && this.selectedUser.isCommitted === 1
-      }).catch((error) => {
-        this.total = 0
-        this.pageNo = 1
-        this.pageLimit = this.pageLimit
-        this.testCaseDatas = []
-        notify('error', '获取测试用例数据失败:' + error.data)
-      })
-      this.hideListLoading()
-    },
-    // handleSearch () {
-    //   this.listQueryParam.pageNo = 1
-    //   this.getList()
-    // },
-    initData () {
-      this.testCaseData = {
-        demand: '',
-        descr: '',
-        envConfig: '',
-        evaCriteria: '',
-        testStatus: '',
-        expectedResult: '',
-        files: [],
-        id: undefined,
-        inputDatas: '',
-        name: '',
-        opeSteps: '',
-        others: '',
-        preconditions: '',
-        priority: '',
-        screenshots: [],
-        taskCode: '',
-        testResult: '',
-        examStatus: '',
-        examDescr: ''
-      }
-    },
-    initDefectData () {
-      this.defectData = {
-        descr: '',
-        envConfig: '',
-        expectedResult: '',
-        files: [],
-        id: undefined,
-        inputDatas: '',
-        opeSteps: '',
-        others: '',
-        preconditions: '',
-        priority: '',
-        seriousness: '',
-        defectType: '',
-        screenshots: [],
-        taskCode: '',
-        testCaseCode: '',
-        testResult: ''
-      }
-    },
-    submitData () {
-      this.$refs.testCaseDetail.submitAuditResult((examStatus, examDescr) => {
-        let testCase = this.testCaseDatas.find(testCase => testCase.id === this.testCaseData.id)
-        testCase.examStatus = examStatus
-        testCase.examDescr = examDescr
-      })
-    },
-    handleDetail (row) {
-      this.dialogFormVisible = true
-      this.deepClone(this.testCaseData, row)
-      this.$nextTick(() => {
-        this.$refs['testCaseDetail'].clearValidate()
-      })
     },
-    handleDefectDetail (row) {
-      this.defectDialogFormVisible = true
-      this.deepClone(this.defectData, row)
+    clearList () {
+      this.$refs.testCaseList.clearList()
+      this.$refs.defectList.clearList()
+      this.$refs.testEnvList.clearList()
+      this.$refs.testToolList.clearList()
     },
     projectDataFilter (val) {
       if (val) {
@@ -434,25 +202,28 @@ export default {
         this.searchProjects = res.data
       }).catch((error) => {
         this.hideLoading()
-        notify('error', '获取项目列表数据失败:' + error.data)
+        notify('error', '获取项目列表数据失败:系统异常')
       })
     },
-    getSimpleTaskDatas () {
-      Http.get(Api.TASK.GET_SIMPLE_DATAS_BY_PROJECT.replace('{projectCode}', this.listQueryParam.selectedProjectCode)).then((res) => {
+    getSimpleTaskDatas (firstIn) {
+      Http.get(Api.TASK.GET_SIMPLE_DATAS_BY_PROJECT.replace('{projectCode}', this.selectedProjectCode)).then((res) => {
         this.tasks = res.data
         this.searchTasks = res.data
-        if (!(this.searchTasks.find(task => task.code === this.listQueryParam.selectedTaskCode))) {
+        if (!firstIn) {
           this.selectedTaskData = this.tasks[0]
-          this.listQueryParam.selectedTaskCode = this.selectedTaskData.code
+          this.selectedTaskCode = this.selectedTaskData.code
+        } else {
+          this.selectedTaskData = this.searchTasks.find(task => task.code === this.selectedTaskCode)
+          this.getTaskUserDatas(firstIn)
         }
       }).catch((error) => {
         this.hideLoading()
-        notify('error', '获取任务列表数据失败:' + error.data)
+        notify('error', '获取任务列表数据失败:系统异常')
       })
     },
-    getTaskUserDatas () {
-      Http.get(Api.TASK.GET_TASK_USER_DATAS.replace('{projectCode}', this.listQueryParam.selectedProjectCode)
-        .replace('{taskCode}', this.listQueryParam.selectedTaskCode)).then((res) => {
+    getTaskUserDatas (firstIn) {
+      Http.get(Api.TASK.GET_TASK_USER_DATAS.replace('{projectCode}', this.selectedProjectCode)
+        .replace('{taskCode}', this.selectedTaskCode)).then((res) => {
         this.users = res.data
         this.users.forEach(user => {
           if (!(user.isCommitted)) {
@@ -461,59 +232,36 @@ export default {
             user.name = user.name + '(已提交)'
           }
         })
-        if (!(this.users.find(user => user.id === this.listQueryParam.selectedUserId))) {
-          this.listQueryParam.selectedUserId = this.users[0].id
-        } else {
-          this.getList()
-        }
-      }).catch((error) => {
-        notify('error', '获取用户列表数据失败:' + error.data)
-      })
-    },
-    deepClone: function (oldData, newData) {
-      if (newData instanceof Array) {
-        if (oldData.length !== newData.length) {
-          oldData = new Array(newData.length)
-        }
-        for (var i = 0; i < newData.length; i++) {
-          if (newData[i] instanceof Array) {
-            oldData[i] = new Array(newData[i].length)
-            this.deepClone(oldData[i], newData[i])
-          } else if (newData[i] instanceof Object) {
-            oldData[i] = {}
-            this.deepClone(oldData[i], newData[i])
+        if (!firstIn) {
+          if (this.users.length === 0) {
+            this.selectedUserId = undefined
+            this.selectedUser = {}
           } else {
-            oldData[i] = newData[i]
+            this.selectedUserId = this.users[0].id
+            this.selectedUser = this.users[0]
           }
-        }
-      } else if (newData instanceof Object) {
-        if (oldData.length > 0) {
-          oldData = {}
-        }
-        for (var key in newData) {
-          if (newData[key] instanceof Array) {
-            oldData[key] = new Array(newData[key].length)
-            this.deepClone(oldData[key], newData[key])
-          } else if (newData[key] instanceof Object) {
-            oldData[key] = {}
-            this.deepClone(oldData[key], newData[key])
+        } else {
+          if (this.selectedUserId) {
+            this.selectedUser = this.users.find(user => user.id === this.selectedUserId)
           } else {
-            oldData[key] = newData[key]
+            this.selectedUser = this.users[0]
+            this.selectedUserId = this.selectedUser.id
           }
+          this.getList(firstIn)
         }
-      }
-      console.log('deepClone')
-      console.log(oldData)
-    },
-    showListLoading () {
-      this.listLoading = true
-    },
-    hideListLoading () {
-      this.listLoading = false
+      }).catch((error) => {
+        console.log(error)
+        notify('error', '获取用户列表数据失败:系统异常')
+      })
     },
-    toogleExpand2 (row) {
-      let testCaseTable = this.$refs.testCaseTable
-      testCaseTable.toggleRowExpansion(row)
+    handleTabClick (tab, event) {
+      if (tab.name === 'testCases') {
+        this.testStatusSearchShow = true
+        this.examStatusSearchShow = true
+      } else {
+        this.testStatusSearchShow = false
+        this.examStatusSearchShow = false
+      }
     }
   }
 }
@@ -524,16 +272,4 @@ export default {
     margin-bottom: 4px;
     padding-left: 6px;
   }
-  .mini-margin {
-    margin-left: 6px;
-    margin-right: 6px;
-  }
-</style>
-<style>
-.el-table__expanded-cell {
-  background-color: #eee!important;
-}
-.el-table__expanded-cell:hover {
-  background-color: #eee!important;
-}
 </style>

+ 156 - 558
src/pages/TestCase/testcases.vue

@@ -1,8 +1,8 @@
 <template>
-  <div class="app-container">
-    <div class="title h1" style="margin-top: 10px;">测试用例管理</div>
+  <div class="app-container" v-loading="loading">
+    <div class="title h1" style="margin-top: 10px;">众测执行</div>
     <div class="filter-container">
-      <el-select v-model="listQueryParam.selectedTaskCode"  filterable :filter-method="dataFilter" @click.native="eqNoClick">
+      <el-select v-model="selectedTaskCode"  filterable :filter-method="dataFilter" @click.native="eqNoClick">
         <el-option
           v-for="task in searchTasks"
           :key="task.code"
@@ -10,382 +10,154 @@
           :value="task.code"
         />
       </el-select>
-      <el-select v-model="listQueryParam.testStatus">
+      <el-select v-if="testStatusSearchShow" v-model="selectedTestStatus">
         <el-option value="" label="---测试结论---"/>
         <el-option value="WAIT" label="待测试"/>
         <el-option value="PASS" label="通过"/>
         <el-option value="NO_PASS" label="不通过"/>
       </el-select>
-      <el-select v-model="listQueryParam.examStatus" v-if="!testCaseCanEdit">
+      <el-select v-model="selectedExamStatus" v-if="!testCaseCanEdit && examStatusSearchShow">
         <el-option value="" label="---审核结果---"/>
         <el-option value="WAIT" label="待审核"/>
         <el-option value="VALID" label="有效"/>
         <el-option value="INVALID" label="无效"/>
       </el-select>
-<!--      <el-input v-model="listQuery.title" placeholder="用例名" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />-->
-<!--      <el-input v-model="listQuery.title" placeholder="用例编号" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />-->
-<!--      <el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleSearch">-->
-<!--        Search-->
-<!--      </el-button>-->
+      <el-upload style="display: inline-block;"
+        :action="uploadTestCasesFileUrl"
+        :on-success="handleUploadTestCasesFileSuccess"
+        :before-upload="beforeTestCasesFileUpload"
+        :on-error="handleUploadTestCasesFileError"
+        :show-file-list="false">
+        <el-button class="filter-item" style="margin-left: 10px;" type="primary" >上传测试用例</el-button>
+      </el-upload>
+      <el-upload style="display: inline-block;"
+                 :action="uploadDefectsFileUrl"
+                 :on-success="handleUploadDefectsFileSuccess"
+                 :before-upload="beforeDefectsFileUpload"
+                 :on-error="handleUploadDefectsFileError"
+                 :show-file-list="false">
+        <el-button class="filter-item" style="margin-left: 10px;" type="primary" >上传缺陷报告</el-button>
+      </el-upload>
       <el-button class="filter-item" style="margin-left: 10px;" type="primary" @click="handleCreate" v-if="testCaseCanEdit">
         新增测试用例
       </el-button>
+      <el-button class="filter-item" style="margin-left: 10px;" type="primary" @click="handleCreateTestEnv" v-if="testCaseCanEdit">
+        新增测试环境
+      </el-button>
+      <el-button class="filter-item" style="margin-left: 10px;" type="primary" @click="handleCreateTestTool" v-if="testCaseCanEdit">
+        新增测试工具
+      </el-button>
       <el-button class="filter-item" style="margin-left: 10px;" type="success" @click="handleSubmitAudit" v-if="testCaseCanEdit">
         提交审核
       </el-button>
+      <el-link type="primary" style="margin-left: 20px" :href="templateDownloadUrl">文档模板下载</el-link>
       <span style="display: block; float: right; color: indianred; margin-right: 20px">提交状态:{{isCommitted ? '已提交' : '未提交'}}</span>
     </div>
 
-    <el-table
-      ref="testCaseTable"
-      :key="tableKey"
-      v-loading="listLoading"
-      :data="testCaseDatas"
-      border
-      fit
-      highlight-current-row
-      style="width: 100%"
-    >
-      <el-table-column type="expand" align="center" min-width="1%">
-        <template slot-scope="{row}">
-          <el-table
-            :key="'innerTableKey' + row.id"
-            :data="row.defects"
-            border
-            fit
-            style="width: 100%"
-          >
-            <el-table-column label="编号" align="center" min-width="15%">
-              <template slot-scope="{row}">
-                <span>{{ row.code }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="缺陷描述" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.descr }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="严重等级" align="center" min-width="12%">
-              <template slot-scope="{row}">
-                <span>{{ toSeriousnessCn(row.seriousness) }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="优先级" align="center" min-width="10%">
-              <template slot-scope="{row}">
-                <span>{{ toPriorityCn(row.priority) }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="缺陷类型" align="center" min-width="12%">
-              <template slot-scope="{row}">
-                <span>{{ toDefectTypeCn(row.defectType) }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作步骤" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.opeSteps }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="输入数据" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.inputDatas }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.expectedResult }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="35%">
-              <template slot-scope="{row}">
-                <span>{{ row.testResult }}</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
-              <template slot-scope="{row,$index}">
-                <i class="el-icon-tickets mini-margin" @click="handleDefectDetail(row)" v-if="!testCaseCanEdit" style="cursor: pointer;" title="查看详情"></i>
-                <i class="el-icon-edit mini-margin " @click="handleUpdateDefect(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
-                <i class="el-icon-delete mini-margin " @click="handleDeleteDefect(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
-                <i class="el-icon-document-copy mini-margin " @click="handleCopyDefect(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="复制"></i>
-              </template>
-            </el-table-column>
-          </el-table>
-        </template>
-      </el-table-column>
-      <el-table-column label="编号" prop="code" sortable="custom" align="center" min-width="3%">
-        <template slot-scope="{row}">
-          <span>{{ row.code }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="用例名称" align="center" :show-overflow-tooltip="true" min-width="6%">
-        <template slot-scope="{row}">
-          <span>{{ row.name }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="优先级" align="center" min-width="3%">
-        <template slot-scope="{row}">
-          <span>{{ toPriorityCn(row.priority) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="描述" align="center" :show-overflow-tooltip="true" min-width="10%">
-        <template slot-scope="{row}">
-          <span>{{ row.descr }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="关联需求" align="center" :show-overflow-tooltip="true" min-width="10%">
-        <template slot-scope="{row}">
-          <span>{{ row.demand }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="8%">
-        <template slot-scope="{row}">
-          <span>{{ row.expectedResult }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="8%">
-        <template slot-scope="{row}">
-          <span>{{ row.testResult }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="测试结论" align="center" min-width="4%">
-        <template slot-scope="{row}">
-          <span>{{ toTestStatusCn(row.testStatus) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="缺陷数量(个)" align="center" min-width="4%">
-        <template slot-scope="{row}">
-          <a href="javascript:void(0)" @click="toogleExpand2(row)" style="color: indianred;">{{ row.defects.length }}</a>
-        </template>
-      </el-table-column>
-      <el-table-column label="审核结果" align="center" min-width="4%" v-if="!testCaseCanEdit">
-        <template slot-scope="{row}">
-          <span>{{ toExamStatusCn(row.examStatus) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="审核结果说明" align="center" :show-overflow-tooltip="true" min-width="8%"  v-if="!testCaseCanEdit">
-        <template slot-scope="{row}">
-          <span>{{ row.examDescr }}</span>
-        </template>
-      </el-table-column>
-<!--      <el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">-->
-<!--        <template slot-scope="{row}">-->
-<!--          <span style="color:red;">{{ row.reviewer }}</span>-->
-<!--        </template>-->
-<!--      </el-table-column>-->
-<!--      <el-table-column label="Imp" width="80px">-->
-<!--        <template slot-scope="{row}">-->
-<!--          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />-->
-<!--        </template>-->
-<!--      </el-table-column>-->
-      <el-table-column label="操作" align="center" min-width="5%">
-        <template slot-scope="{row,$index}">
-          <i class="el-icon-tickets mini-margin" @click="handleDetail(row)" v-if="!testCaseCanEdit" style="cursor: pointer;" title="查看详情"></i>
-          <i class="el-icon-edit mini-margin" @click="handleUpdate(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
-          <i class="el-icon-delete mini-margin" @click="handleDelete(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
-          <i class="el-icon-document-copy mini-margin" @click="handleCopy(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="复制"></i>
-          <i class="el-icon-document-add mini-margin" @click="handleCreateDefect(row)" v-if="testCaseCanEdit && row.testStatus === 'NO_PASS'" style="cursor: pointer;" title="新增缺陷"></i>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination v-show="total>0" :total="total" :page.sync="listQueryParam.pageNo" :limit.sync="listQueryParam.pageSize" @pagination="getList" />
-
-    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
-      <testcase-form ref="testCaseForm" style="width: 600px;" :test-case-data="testCaseData" :read-only="!testCaseCanEdit"></testcase-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="dialogFormVisible = false">
-          取消
-        </el-button>
-        <el-button type="primary" @click="submitData()">
-          提交
-        </el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog :title="textMap[dialogStatus]" :visible.sync="defectDialogFormVisible">
-      <defect-form ref="defectForm" style="width: 600px;" :defect-data="defectData"></defect-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="defectDialogFormVisible = false">
-          取消
-        </el-button>
-        <el-button type="primary" @click="submitDefectData()">
-          提交
-        </el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog title="用例详情" :visible.sync="testCaseDetailDialogFormVisible">
-      <testcase-detail ref="testCaseDetail" style="width: 600px;" :test-case-data="testCaseData" :can-audit="false"></testcase-detail>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="testCaseDetailDialogFormVisible = false">
-          取消
-        </el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog title="缺陷详情" :visible.sync="defectDetailDialogFormVisible">
-      <defect-detail ref="defectDetail" style="width: 600px;" :defect-data="defectData"></defect-detail>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="defectDetailDialogFormVisible = false">
-          取消
-        </el-button>
-      </div>
-    </el-dialog>
+    <el-tabs v-model="tabActiveName" @tab-click="handleTabClick">
+      <el-tab-pane label="测试用例" name="testCases">
+        <test-case-list ref="testCaseList" :selected-task-code="selectedTaskCode" :selected-test-status="selectedTestStatus" :selected-exam-status="selectedExamStatus" :test-case-can-edit="testCaseCanEdit"></test-case-list>
+      </el-tab-pane>
+      <el-tab-pane label="用例缺陷" name="defects">
+        <defect-list ref="defectList" :isContained="false" :test-case-can-edit="testCaseCanEdit" :selected-task-code="selectedTaskCode"/>
+      </el-tab-pane>
+      <el-tab-pane label="测试环境" name="testEnvs">
+        <test-env-list ref="testEnvList" :test-case-can-edit="testCaseCanEdit" :selected-task-code="selectedTaskCode"></test-env-list>
+      </el-tab-pane>
+      <el-tab-pane label="测试工具" name="testTools">
+        <test-tool-list ref="testToolList" :test-case-can-edit="testCaseCanEdit" :selected-task-code="selectedTaskCode"></test-tool-list>
+      </el-tab-pane>
+    </el-tabs>
   </div>
 </template>
 
 <script>
-import waves from '@/directive/waves' // waves directive
-import Pagination from '@/components/Pagination' // secondary package based on el-pagination
-import TestcaseForm from './components/testcase_form'
-import DefectForm from './components/defect_form'
-import TestcaseDetail from './components/testcase_detail'
-import DefectDetail from './components/defect_detail'
+import TestCaseList from './components/test_case_list'
+import DefectList from './components/defect_list'
+import TestEnvList from './components/test_env_list'
+import TestToolList from './components/test_tool_list'
 import Http from '@/js/http'
 import Api from '@/js/api'
 import {notify} from '@/constants'
-import TestCaseUtils from './utils'
+import { mapActions, mapGetters } from 'vuex'
 
 export default {
-  name: 'ComplexTable',
-  components: { Pagination, TestcaseForm, DefectForm, TestcaseDetail, DefectDetail },
-  directives: { waves },
+  name: 'TestCases',
+  components: { TestCaseList, DefectList, TestEnvList, TestToolList },
   data () {
     return {
-      testCaseData: {},
-      defectData: {},
-      testCaseDatas: [],
-      tableKey: 0,
-      total: 0,
-      listQueryParam: {
-        pageNo: 1,
-        pageSize: 20,
-        selectedTaskCode: this.$route.params.taskCode,
-        testStatus: '',
-        examStatus: ''
-      },
-      listLoading: true,
-      showReviewer: false,
-      dialogFormVisible: false,
-      defectDialogFormVisible: false,
-      testCaseDetailDialogFormVisible: false,
-      defectDetailDialogFormVisible: false,
-      dialogStatus: '',
-      textMap: {
-        update: '编辑测试用例',
-        create: '新建测试用例',
-        create_defect: '新建缺陷',
-        update_defect: '编辑缺陷',
-        detail: '测试用例详情',
-        defect_detail: '缺陷详情'
-      },
-      selectedTaskCode: '',
+      tabActiveName: 'testCases',
+      selectedTaskCode: this.$route.params.taskCode,
+      selectedTestStatus: '',
+      selectedExamStatus: '',
       tasks: [],
       searchTasks: [],
       testCaseCanEdit: false,
-      isCommitted: false
+      isCommitted: false,
+      testStatusSearchShow: true,
+      examStatusSearchShow: true,
+      loading: false,
+      templateDownloadUrl: process.env.OSS_URL + '%E6%96%87%E6%A1%A3%E6%A8%A1%E6%9D%BF.rar'
+    }
+  },
+  computed: {
+    uploadTestCasesFileUrl () {
+      return process.env.API_ROOT + Api.TESTCASE.UPLOAD_TEST_CASES_FILE.replace('{taskCode}', this.selectedTaskCode)
+    },
+    uploadDefectsFileUrl () {
+      return process.env.API_ROOT + Api.TESTCASE.UPLOAD_DEFECTS_FILE.replace('{taskCode}', this.selectedTaskCode)
     }
   },
   watch: {
-    'listQueryParam.selectedTaskCode': function (newVal, oldVal) {
+    'selectedTaskCode': function (newVal, oldVal) {
       this.getList()
       this.handleStatus()
     },
-    'listQueryParam.testStatus': function (newVal, oldVal) {
+    'selectedTestStatus': function (newVal, oldVal) {
       this.getList()
     },
-    'listQueryParam.examStatus': function (newVal, oldVal) {
+    'selectedExamStatus': function (newVal, oldVal) {
       this.getList()
     }
   },
   created () {
-    this.initData()
-    this.initDefectData()
     this.getSimpleTaskDatas()
-    this.getList()
+    this.$nextTick(() => {
+      this.setRefreshTestCaseListFunc(this.$refs.testCaseList.getList)
+      this.setRefreshDefectListFunc(this.$refs.defectList.getList)
+    })
   },
   methods: {
-    ...TestCaseUtils,
-    handleStatus () {
-      let task = this.tasks.find(task => {
-        return task.code === this.listQueryParam.selectedTaskCode
+    ...mapActions(['setRefreshTestCaseListFunc', 'setRefreshDefectListFunc']),
+    ...mapGetters(['getRefreshTestCaseListFunc', 'getRefreshDefectListFunc']),
+    getTestCaseListMethod () {
+      this.$nextTick(() => {
+        return this.$refs.testCaseList.getList
       })
-      this.testCaseCanEdit = !(task.isCommitted) && task.status !== 5
-      this.isCommitted = task.isCommitted && true
     },
-    getList () {
-      this.showListLoading()
-      let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
-        .replace('{designerId}', '0')
-        .replace('{pageNo}', this.listQueryParam.pageNo - 1)
-        .replace('{pageSize}', this.listQueryParam.pageSize)
-      if (this.listQueryParam.testStatus || this.listQueryParam.examStatus) {
-        if (url.indexOf('?') === -1) {
-          url = url + '?'
-        }
-        if (this.listQueryParam.testStatus) {
-          url = url + 'testStatus=' + this.listQueryParam.testStatus + '&'
-        }
-        if (this.listQueryParam.examStatus) {
-          url = url + 'examStatus=' + this.listQueryParam.examStatus + '&'
-        }
-        url = url.substring(0, url.length - 1)
-      }
-      Http.get(url).then((res) => {
-        const testCasePage = res.data
-        this.total = testCasePage.totalCount
-        this.listQueryParam.pageNo = testCasePage.pageNo + 1
-        this.listQueryParam.pageSize = testCasePage.pageSize
-        this.testCaseDatas = testCasePage.datas
-      }).catch((error) => {
-        console.error(error)
-        notify('error', '获取测试用例数据失败:' + error)
+    getDefectListMethod () {
+      this.$nextTick(() => {
+        return this.$refs.defectList.getList
       })
-      this.hideListLoading()
     },
-    // handleSearch () {
-    //   this.listQueryParam.pageNo = 1
-    //   this.getList()
-    // },
-    initData () {
-      this.testCaseData = {
-        demand: '',
-        descr: '',
-        envConfig: '',
-        evaCriteria: '',
-        testStatus: '',
-        expectedResult: '',
-        files: [],
-        id: undefined,
-        inputDatas: '',
-        name: '',
-        opeSteps: '',
-        others: '',
-        preconditions: '',
-        priority: '',
-        screenshots: [],
-        taskCode: '',
-        testResult: ''
-      }
+    getList () {
+      this.$refs.testCaseList.listQueryParam.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testCaseList.listQueryParam.testStatus = this.selectedTestStatus
+      this.$refs.testCaseList.listQueryParam.examStatus = this.selectedExamStatus
+      this.$refs.defectList.listQueryParam.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testEnvList.listQueryParam.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testToolList.selectedTaskCode = this.selectedTaskCode
+      this.$refs.testCaseList.getList()
+      this.$refs.defectList.getList()
+      this.$refs.testEnvList.getList()
+      this.$refs.testToolList.getList()
     },
-    initDefectData () {
-      this.defectData = {
-        descr: '',
-        envConfig: '',
-        expectedResult: '',
-        files: [],
-        id: undefined,
-        inputDatas: '',
-        opeSteps: '',
-        others: '',
-        preconditions: '',
-        priority: '',
-        seriousness: '',
-        defectType: '',
-        screenshots: [],
-        taskCode: '',
-        testCaseCode: '',
-        testResult: ''
-      }
+    handleStatus () {
+      let task = this.tasks.find(task => {
+        return task.code === this.selectedTaskCode
+      })
+      this.testCaseCanEdit = !(task.isCommitted) && task.status !== 5
+      this.isCommitted = task.isCommitted && true
     },
     handleSubmitAudit () {
       this.$confirm('提交任务之后将不能再编辑测试用例,您确定要提交该任务吗', '提示', {
@@ -395,197 +167,32 @@ export default {
         center: true,
         closeOnClickModal: false
       }).then(() => {
-        this.showListLoading()
-        Http.put(Api.TASK.COMMIT_TASK.replace('{taskCode}', this.listQueryParam.selectedTaskCode), {}).then((res) => {
-          this.hideListLoading()
+        Http.put(Api.TASK.COMMIT_TASK.replace('{taskCode}', this.selectedTaskCode), {}).then((res) => {
           if (res.code === 20000) {
-            this.$notify({
-              title: 'Success',
-              message: '提交成功',
-              type: 'success',
-              duration: 2000
-            })
+            notify('success', '提交成功')
             this.testCaseCanEdit = false
+            this.isCommitted = true
           } else {
-            this.$notify({
-              title: 'Error',
-              message: '提交失败:' + res.data,
-              type: 'Error',
-              duration: 2000
-            })
+            notify('error', '提交失败:' + res.data)
           }
         }).catch((error) => {
           this.hideListLoading()
-          notify('error', '提交失败:' + error)
+          notify('error', '提交失败:系统异常')
         })
       }).catch(() => {
       })
     },
     handleCreate () {
-      this.initData()
-      this.dialogStatus = 'create'
-      this.dialogFormVisible = true
-      this.testCaseData.taskCode = this.listQueryParam.selectedTaskCode
-      this.$nextTick(() => {
-        this.$refs['testCaseForm'].clearValidate()
-      })
-    },
-    handleCreateDefect (testCase) {
-      this.initDefectData()
-      this.dialogStatus = 'create_defect'
-      this.defectDialogFormVisible = true
-      this.defectData.taskCode = this.listQueryParam.selectedTaskCode
-      this.defectData.testCaseCode = testCase.code
-      this.defectData.priority = testCase.priority
-      this.defectData.preconditions = testCase.preconditions
-      this.defectData.envConfig = testCase.envConfig
-      this.defectData.opeSteps = testCase.opeSteps
-      this.defectData.inputDatas = testCase.inputDatas
-      this.defectData.expectedResult = testCase.expectedResult
-      this.defectData.testResult = testCase.testResult
-      this.$nextTick(() => {
-        this.$refs['defectForm'].clearValidate()
-      })
-    },
-    submitData () {
-      let callback = null
-      if (this.testCaseData.id) {
-        callback = this.getList
-      } else {
-        callback = () => {
-          this.listQueryParam.pageNo = Math.ceil((this.total + 1) / this.listQueryParam.pageSize)
-          this.getList()
-        }
-      }
-      this.$refs.testCaseForm.submitForm(callback)
-    },
-    submitDefectData () {
-      this.$refs.defectForm.submitForm(this.getList)
-    },
-    handleDetail (row) {
-      this.testCaseDetailDialogFormVisible = true
-      this.deepClone(this.testCaseData, row)
-    },
-    handleDefectDetail (row) {
-      this.defectDetailDialogFormVisible = true
-      this.deepClone(this.defectData, row)
-    },
-    handleUpdate (row) {
-      this.dialogStatus = 'update'
-      this.dialogFormVisible = true
-      this.deepClone(this.testCaseData, row)
-      this.$nextTick(() => {
-        this.$refs['testCaseForm'].clearValidate()
-      })
-    },
-    handleUpdateDefect (row) {
-      this.dialogStatus = 'update_defect'
-      this.defectDialogFormVisible = true
-      this.deepClone(this.defectData, row)
-      this.$nextTick(() => {
-        this.$refs['defectForm'].clearValidate()
-      })
-      console.log(this.defectData)
-    },
-    handleDelete (row, index) {
-      this.$confirm('您确定要删除该条数据吗?', '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning',
-        center: true,
-        closeOnClickModal: false
-      }).then(() => {
-        this.showListLoading()
-        Http.delete(Api.TESTCASE.DELETE.replace('{id}', row.id), {}).then((res) => {
-          this.hideListLoading()
-          if (res.code === 20000) {
-            this.$notify({
-              title: 'Success',
-              message: '删除成功',
-              type: 'success',
-              duration: 2000
-            })
-            this.getList()
-          } else {
-            this.$notify({
-              title: 'Error',
-              message: '删除失败' + res.msg,
-              type: 'Error',
-              duration: 2000
-            })
-          }
-        }).catch((error) => {
-          this.hideListLoading()
-          notify('error', '删除失败:' + error)
-        })
-      }).catch(() => {
-      })
+      this.tabActiveName = 'testCases'
+      this.$refs.testCaseList.handleCreate()
     },
-    handleDeleteDefect (row, index) {
-      this.$confirm('您确定要删除该条数据吗?', '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning',
-        center: true,
-        closeOnClickModal: false
-      }).then(() => {
-        this.showListLoading()
-        Http.delete(Api.TESTCASE.DELETE_DEFECT.replace('{id}', row.id), {}).then((res) => {
-          this.hideListLoading()
-          if (res.code === 20000) {
-            this.$notify({
-              title: 'Success',
-              message: '删除成功',
-              type: 'success',
-              duration: 2000
-            })
-            for (let i in this.testCaseDatas) {
-              let testCase = this.testCaseDatas[i]
-              if (testCase.defects.length > index) {
-                if (testCase.defects[index].id === row.id) {
-                  testCase.defects.splice(index, 1)
-                  break
-                }
-              }
-            }
-          } else {
-            this.$notify({
-              title: 'Error',
-              message: '删除失败' + res.msg,
-              type: 'Error',
-              duration: 2000
-            })
-          }
-        }).catch((error) => {
-          this.hideListLoading()
-          notify('error', '删除失败:' + error)
-        })
-      }).catch(() => {
-      })
-    },
-    handleCopy (row) {
-      this.dialogStatus = 'create'
-      this.dialogFormVisible = true
-      this.deepClone(this.testCaseData, row)
-      this.testCaseData.id = undefined
-      this.$nextTick(() => {
-        this.$refs['testCaseForm'].clearValidate()
-      })
-      console.log(this.testCaseData)
-    },
-    handleCopyDefect (row) {
-      this.dialogStatus = 'create_defect'
-      this.defectDialogFormVisible = true
-      this.deepClone(this.defectData, row)
-      this.defectData.id = undefined
-      this.$nextTick(() => {
-        this.$refs['defectForm'].clearValidate()
-      })
-      console.log(this.defectData)
+    handleCreateTestEnv () {
+      this.tabActiveName = 'testEnvs'
+      this.$refs.testEnvList.handleCreate()
     },
-    getSortClass: function (key) {
-      const sort = this.listQuery.sort
-      return sort === `+${key}` ? 'ascending' : 'descending'
+    handleCreateTestTool () {
+      this.tabActiveName = 'testTools'
+      this.$refs.testToolList.handleCreate()
     },
     dataFilter (val) {
       if (val) {
@@ -612,54 +219,57 @@ export default {
         this.searchTasks = this.tasks
         this.handleStatus()
       }).catch((error) => {
-        this.hideLoading()
-        notify('error', '获取任务数据失败:' + error.data)
+        notify('error', '获取任务数据失败:系统异常')
       })
     },
-    deepClone: function (oldData, newData) {
-      if (newData instanceof Array) {
-        if (oldData.length !== newData.length) {
-          oldData = new Array(newData.length)
-        }
-        for (var i = 0; i < newData.length; i++) {
-          if (newData[i] instanceof Array) {
-            oldData[i] = new Array(newData[i].length)
-            this.deepClone(oldData[i], newData[i])
-          } else if (newData[i] instanceof Object) {
-            oldData[i] = {}
-            this.deepClone(oldData[i], newData[i])
-          } else {
-            oldData[i] = newData[i]
-          }
-        }
-      } else if (newData instanceof Object) {
-        if (oldData.length > 0) {
-          oldData = {}
-        }
-        for (var key in newData) {
-          if (newData[key] instanceof Array) {
-            oldData[key] = new Array(newData[key].length)
-            this.deepClone(oldData[key], newData[key])
-          } else if (newData[key] instanceof Object) {
-            oldData[key] = {}
-            this.deepClone(oldData[key], newData[key])
-          } else {
-            oldData[key] = newData[key]
-          }
-        }
+    handleTabClick (tab, event) {
+      if (tab.name === 'testCases') {
+        this.testStatusSearchShow = true
+        this.examStatusSearchShow = true
+      } else {
+        this.testStatusSearchShow = false
+        this.examStatusSearchShow = false
+      }
+    },
+    beforeTestCasesFileUpload (file) {
+      const isXLSX = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+      if (!(isXLSX)) {
+        this.$message.error('上传文件只能是XLSX 格式!')
       }
-      console.log('deepClone')
-      console.log(oldData)
+      if (isXLSX) {
+        this.loading = true
+      }
+      return isXLSX
+    },
+    handleUploadTestCasesFileSuccess (response, file, fileList) {
+      this.loading = false
+      notify('success', '上传成功')
+      this.getRefreshTestCaseListFunc()()
+    },
+    handleUploadTestCasesFileError (err, file, fileList) {
+      console.log(err)
+      this.loading = false
+      notify('error', '上传失败' + err)
     },
-    showListLoading () {
-      this.listLoading = true
+    beforeDefectsFileUpload (file) {
+      const isXLSX = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+      if (!(isXLSX)) {
+        this.$message.error('上传文件只能是XLSX 格式!')
+      }
+      if (isXLSX) {
+        this.loading = true
+      }
+      return isXLSX
     },
-    hideListLoading () {
-      this.listLoading = false
+    handleUploadDefectsFileSuccess (response, file, fileList) {
+      this.loading = false
+      notify('success', '上传成功')
+      this.getRefreshDefectListFunc()()
     },
-    toogleExpand2 (row) {
-      let testCaseTable = this.$refs.testCaseTable
-      testCaseTable.toggleRowExpansion(row)
+    handleUploadDefectsFileError (err, file, fileList) {
+      console.log(err)
+      this.loading = false
+      notify('error', '上传失败' + err)
     }
   }
 }
@@ -670,16 +280,4 @@ export default {
     margin-bottom: 4px;
     padding-left: 6px;
   }
-  .mini-margin {
-    margin-left: 6px;
-    margin-right: 6px;
-  }
-</style>
-<style>
-.el-table__expanded-cell {
-  background-color: #eee!important;
-}
-.el-table__expanded-cell:hover {
-  background-color: #eee!important;
-}
 </style>

+ 5 - 5
src/pages/TestCase/utils/index.js

@@ -35,15 +35,15 @@ export default {
   toSeriousnessCn (seriousness) {
     var seriousnessCn = ''
     if (seriousness === 'VERY_HIGH') {
-      seriousnessCn = '极高'
+      seriousnessCn = '致命'
     } else if (seriousness === 'HIGH') {
-      seriousnessCn = ''
+      seriousnessCn = '严重'
     } else if (seriousness === 'MID') {
-      seriousnessCn = ''
+      seriousnessCn = '一般'
     } else if (seriousness === 'LOW') {
-      seriousnessCn = ''
+      seriousnessCn = '轻微'
     } else if (seriousness === 'VERY_LOW') {
-      seriousnessCn = '极低'
+      seriousnessCn = '建议'
     }
     return seriousnessCn
   },

+ 98 - 0
src/pages/Tester/cloud-data.js

@@ -0,0 +1,98 @@
+export const wordCloudData =  [
+  {
+    name: "十九大精神",
+    value: 15000
+  },
+  {
+    name: "两学一做",
+    value: 10081
+  },
+  {
+    name: "中华民族",
+    value: 9386
+  },
+  {
+    name: "马克思主义",
+    value: 7500
+  },
+  {
+    name: "民族复兴",
+    value: 7500
+  },
+  {
+    name: "社会主义制度",
+    value: 6500
+  },
+  {
+    name: "国防白皮书",
+    value: 6500
+  },
+  {
+    name: "创新",
+    value: 6000
+  },
+  {
+    name: "民主革命",
+    value: 4500
+  },
+  {
+    name: "文化强国",
+    value: 3800
+  },
+  {
+    name: "国家主权",
+    value: 3000
+  },
+  {
+    name: "武装颠覆",
+    value: 2500
+  },
+  {
+    name: "领土完整",
+    value: 2300
+  },
+  {
+    name: "安全",
+    value: 2000
+  },
+  {
+    name: "从严治党",
+    value: 1900
+  },
+  {
+    name: "现代化经济体系",
+    value: 1800
+  },
+  {
+    name: "国防动员",
+    value: 1700
+  },
+  {
+    name: "信息化战争",
+    value: 1600
+  },
+  {
+    name: "局部战争",
+    value: 1500
+  },
+  {
+    name: "教育",
+    value: 1200
+  },
+  {
+    name: "职业教育",
+    value: 1100
+  },
+  {
+    name: "兵法",
+    value: 900
+  },
+  {
+    name: "一国两制",
+    value: 800
+  },
+  {
+    name: "特色社会主义思想",
+    value: 700
+  },
+]

+ 102 - 0
src/pages/Tester/components/BarChart.vue

@@ -0,0 +1,102 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</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'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          top: 10,
+          left: '2%',
+          right: '2%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: [{
+          type: 'category',
+          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+          axisTick: {
+            alignWithLabel: true
+          }
+        }],
+        yAxis: [{
+          type: 'value',
+          axisTick: {
+            show: false
+          }
+        }],
+        series: [{
+          name: 'pageA',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [79, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }, {
+          name: 'pageB',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [80, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }, {
+          name: 'pageC',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [30, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>

+ 111 - 0
src/pages/Tester/components/BoxCard.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-card class="box-card-component" style="margin-left:8px;">
+    <div slot="header" class="box-card-header">
+      <img src="https://wpimg.wallstcn.com/e7d23d71-cf19-4b90-a1cc-f56af8c0903d.png">
+    </div>
+    <div style="position:relative;">
+      <pan-thumb :image="avatar" class="panThumb" />
+      <mallki class-name="mallki-text" text="报告数据情况" />
+      <div style="padding-top:35px;" class="progress-item">
+        <span>点赞报告数</span>
+        <el-progress :percentage="70" />
+      </div>
+      <div class="progress-item">
+        <span>点踩报告数</span>
+        <el-progress :percentage="18" />
+      </div>
+      <div class="progress-item">
+        <span>被fork报告数</span>
+        <el-progress :percentage="12" />
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+export default {
+
+  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 {
+  .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;
+    right: 0px;
+    font-size: 20px;
+    font-weight: bold;
+  }
+  .panThumb {
+    z-index: 100;
+    height: 70px!important;
+    width: 70px!important;
+    position: absolute!important;
+    top: -45px;
+    left: 0px;
+    border: 5px solid #ffffff;
+    background-color: #fff;
+    margin: auto;
+    box-shadow: none!important;
+    ::v-deep .pan-info {
+      box-shadow: none!important;
+    }
+  }
+  .progress-item {
+    margin-bottom: 10px;
+    font-size: 14px;
+  }
+  @media only screen and (max-width: 1510px){
+    .mallki-text{
+      display: none;
+    }
+  }
+}
+</style>

+ 114 - 0
src/pages/Tester/components/BugLevelChart.vue

@@ -0,0 +1,114 @@
+<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' // echarts theme
+import resize from './mixins/resize'
+require('echarts/theme/macarons')
+
+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 = nv
+        this.initChart()
+      },
+      deep: true,
+      immediate: 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>

+ 116 - 0
src/pages/Tester/components/InfoCard.vue

@@ -0,0 +1,116 @@
+<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="120px" class="demo-ruleForm">
+        <el-form-item label="用户头像:">
+          <el-avatar :size="70" :src="info.photoUrl"></el-avatar>
+        </el-form-item>
+        <el-form-item label="用户姓名:">
+          {{info.name}}
+        </el-form-item>
+        <el-form-item label="所在城市:">
+          {{info.province}} {{info.city}}
+        </el-form-item>
+        <el-form-item label="所在单位:">
+          {{info.unit}}
+        </el-form-item>
+        <el-form-item label="注册时间:">
+          {{info.createTime}}
+        </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:['info'],
+  filters: {
+    statusFilter(status) {c
+      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 {
+  .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;
+  }
+  .panThumb {
+    z-index: 100;
+    height: 70px!important;
+    width: 70px!important;
+    position: absolute!important;
+    top: -45px;
+    left: 0px;
+    border: 5px solid #ffffff;
+    background-color: #fff;
+    margin: auto;
+    box-shadow: none!important;
+    ::v-deep .pan-info {
+      box-shadow: none!important;
+    }
+  }
+  .progress-item {
+    margin-bottom: 10px;
+    font-size: 14px;
+  }
+  @media only screen and (max-width: 1510px){
+    .mallki-text{
+      display: none;
+    }
+  }
+}
+</style>

+ 135 - 0
src/pages/Tester/components/LineChart.vue

@@ -0,0 +1,135 @@
+<template>
+  <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: '350px'
+    },
+    autoResize: {
+      type: Boolean,
+      default: true
+    },
+    chartData: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  watch: {
+    chartData: {
+      deep: true,
+      handler(val) {
+        this.setOptions(val)
+      }
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+      this.setOptions(this.chartData)
+    },
+    setOptions({ expectedData, actualData } = {}) {
+      this.chart.setOption({
+        xAxis: {
+          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+          boundaryGap: false,
+          axisTick: {
+            show: false
+          }
+        },
+        grid: {
+          left: 10,
+          right: 10,
+          bottom: 20,
+          top: 30,
+          containLabel: true
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross'
+          },
+          padding: [5, 10]
+        },
+        yAxis: {
+          axisTick: {
+            show: false
+          }
+        },
+        legend: {
+          data: ['expected', 'actual']
+        },
+        series: [{
+          name: 'expected', itemStyle: {
+            normal: {
+              color: '#FF005A',
+              lineStyle: {
+                color: '#FF005A',
+                width: 2
+              }
+            }
+          },
+          smooth: true,
+          type: 'line',
+          data: expectedData,
+          animationDuration: 2800,
+          animationEasing: 'cubicInOut'
+        },
+        {
+          name: 'actual',
+          smooth: true,
+          type: 'line',
+          itemStyle: {
+            normal: {
+              color: '#3888fa',
+              lineStyle: {
+                color: '#3888fa',
+                width: 2
+              },
+              areaStyle: {
+                color: '#f3f8ff'
+              }
+            }
+          },
+          data: actualData,
+          animationDuration: 2800,
+          animationEasing: 'quadraticOut'
+        }]
+      })
+    }
+  }
+}
+</script>

+ 181 - 0
src/pages/Tester/components/PanelGroup.vue

@@ -0,0 +1,181 @@
+<template>
+  <el-row :gutter="40" class="panel-group">
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('newVisitis')">
+        <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="102400" :duration="2600" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('messages')">
+        <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="81212" :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" @click="handleSetLineChartData('purchases')">
+        <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="9280" :duration="3200" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('shoppings')">
+        <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="13600" :duration="3600" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import CountTo from 'vue-count-to'
+
+export default {
+  components: {
+    CountTo
+  },
+  methods: {
+    handleSetLineChartData(type) {
+      this.$emit('handleSetLineChartData', type)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.panel-group {
+  margin-top: 18px;
+
+  .card-panel-col {
+    margin-bottom: 32px;
+  }
+
+  .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-people {
+      color: #40c9c6;
+    }
+
+    .icon-report {
+      color: #36a3f7;
+    }
+
+    .icon-bug {
+      color: #f4516c;
+    }
+
+    .icon-rate {
+      color: #ffa30c
+    }
+
+    .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;
+      }
+    }
+  }
+}
+
+@media (max-width:550px) {
+  .card-panel-description {
+    display: none;
+  }
+
+  .card-panel-icon-wrapper {
+    float: none !important;
+    width: 100%;
+    height: 100%;
+    margin: 0 !important;
+
+    .svg-icon {
+      display: block;
+      margin: 14px auto !important;
+      float: none !important;
+    }
+  }
+}
+</style>

+ 79 - 0
src/pages/Tester/components/PieChart.vue

@@ -0,0 +1,79 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</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'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'item',
+          formatter: '{a} <br/>{b} : {c} ({d}%)'
+        },
+        legend: {
+          left: 'center',
+          bottom: '10',
+          data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
+        },
+        series: [
+          {
+            name: 'WEEKLY WRITE ARTICLES',
+            type: 'pie',
+            roseType: 'radius',
+            radius: [15, 95],
+            center: ['50%', '38%'],
+            data: [
+              { value: 320, name: 'Industries' },
+              { value: 240, name: 'Technology' },
+              { value: 149, name: 'Forex' },
+              { value: 100, name: 'Gold' },
+              { value: 59, name: 'Forecasts' }
+            ],
+            animationEasing: 'cubicInOut',
+            animationDuration: 2600
+          }
+        ]
+      })
+    }
+  }
+}
+</script>

+ 100 - 0
src/pages/Tester/components/RaddarChart.vue

@@ -0,0 +1,100 @@
+<template>
+  <div style="height: 100%">
+    <div class="chart-title">个人能力雷达图</div>
+<!--    <div id="radderChart" :class="className" :style="{height:height,width:width}"/>-->
+    <div id="radderChart" :class="className" :style="{height:height,width:width}"></div>
+  </div>
+</template>
+
+<script>
+  import echarts from 'echarts'
+
+  require('echarts/theme/macarons') // echarts theme
+  import resize from './mixins/resize'
+
+  const animationDuration = 3000
+
+  export default {
+    mixins: [resize],
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '100%'
+      },
+      height: {
+        type: String,
+        default: '300px'
+      },
+      info: {
+        type: Object,
+        default: {}
+      }
+    },
+    data() {
+      return {
+        chart: null
+      }
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.initChart()
+      })
+    },
+    beforeDestroy() {
+      if (!this.chart) {
+        return
+      }
+      this.chart.dispose()
+      this.chart = null
+    },
+    methods: {
+      initChart() {
+        this.chart = echarts.init(document.getElementById('radderChart'), 'macarons')
+
+        this.chart.setOption(
+          {
+            tooltip: {},
+            // legend: {
+            //   data: ['预算分配(Allocated Budget)', '实际开销(Actual Spending)']
+            // },
+            radar: {
+              // shape: 'circle',
+              name: {
+                textStyle: {
+                  color: '#fff',
+                  backgroundColor: '#baadde',
+                  borderRadius: 3,
+                  padding: [3, 5]
+                }
+              },
+              indicator:this.info.labels.map(item=>{
+                return {name:[item],max:10}
+              })
+            },
+            series: [{
+              type: 'radar',
+              data: [
+                {
+                  value: this.info.data,
+                  name: '个人能力雷达图'
+                }
+              ]
+            }]
+          }
+        )
+      }
+    }
+  }
+</script>
+<style>
+  .chart-title {
+    padding: 5px 8px 15px 8px;
+    font-weight: bold;
+    font-size: 18px;
+  }
+
+</style>

+ 33 - 0
src/pages/Tester/components/TimeLine.vue

@@ -0,0 +1,33 @@
+<template>
+  <el-card>
+    <timeline>
+      <timeline-item :key="'timerline' + item.id" bg-color="#9dd8e0" v-for="item in list">
+        <span class="bug-time">{{item.commitTime}}</span>
+        <el-card style="background-color: #eeeeee">
+          <div>{{item.descr}}</div>
+        </el-card>
+      </timeline-item>
+    </timeline>
+  </el-card>
+</template>
+
+<script>
+import { Timeline, TimelineItem, TimelineTitle } from 'vue-cute-timeline'
+
+export default {
+  name: 'TimeLine',
+  props: ['list'],
+  components: {
+    Timeline,
+    TimelineItem,
+    TimelineTitle
+  }
+}
+</script>
+
+<style scoped>
+.bug-time {
+  display: block;
+  margin-bottom: 10px;
+}
+</style>

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