3 Revīzijas 3ad99401c5 ... a5f2acc2c2

Autors SHA1 Ziņojums Datums
  linyk a5f2acc2c2 增加了个人证书和任务金额计算的功能 2 gadi atpakaļ
  linyk 674910f7ab Merge branch 'dev' of git.mooctest.com:cofortest/cofortest-frontend 2 gadi atpakaļ
  linyk d40391f61a 修复提交表单图片缓存的问题 2 gadi atpakaļ
36 mainītis faili ar 2474 papildinājumiem un 61 dzēšanām
  1. 4 1
      config/dev.env.js
  2. 4 1
      config/prod.env.js
  3. 4 1
      config/test.env.js
  4. 83 0
      package-lock.json
  5. 1 0
      package.json
  6. 1 1
      src/components/file/FileUpload.vue
  7. 1 1
      src/components/file/ImgUpload.vue
  8. 83 12
      src/components/project/Project.vue
  9. 18 0
      src/components/task/Task.vue
  10. 36 3
      src/js/api.js
  11. 268 0
      src/pages/Amount/TaskAmountCal.vue
  12. 93 0
      src/pages/Statistics/Statistics.vue
  13. 3 3
      src/pages/Statistics/TaskStatistics.vue
  14. 103 0
      src/pages/Statistics/components/AppTypeChart.vue
  15. 164 0
      src/pages/Statistics/components/Count.vue
  16. 3 3
      src/pages/Statistics/components/PanelGroup.vue
  17. 104 0
      src/pages/Statistics/components/ProjectFieldChart.vue
  18. 137 0
      src/pages/Statistics/components/ProjectTypeCountChart.vue
  19. 82 0
      src/pages/Statistics/components/ProvinceTable.vue
  20. 115 0
      src/pages/Statistics/components/TaskMonthCountChart.vue
  21. 1 1
      src/pages/Statistics/components/map/StaffDistributionMap.vue
  22. 156 0
      src/pages/Statistics/components/map/TotalMap.vue
  23. 0 0
      src/pages/Statistics/components/map/map-data.js
  24. 3 0
      src/pages/TestCase/components/defect_form.vue
  25. 13 2
      src/pages/TestCase/components/defect_list.vue
  26. 238 0
      src/pages/TestCase/components/mix_defect_list.vue
  27. 261 0
      src/pages/TestCase/components/mix_supp_defect_list.vue
  28. 40 6
      src/pages/TestCase/components/test_case_list.vue
  29. 2 2
      src/pages/TestCase/components/test_tool_list.vue
  30. 10 4
      src/pages/TestCase/components/testcase_form.vue
  31. 27 2
      src/pages/TestCase/exam_testcases.vue
  32. 175 0
      src/pages/TestCase/mix_defects.vue
  33. 18 18
      src/pages/TestCase/testcases.vue
  34. 31 0
      src/pages/UserCenter/Cert.vue
  35. 173 0
      src/pages/UserCenter/components/TesterCert.vue
  36. 19 0
      src/router/index.js

+ 4 - 1
config/dev.env.js

@@ -17,5 +17,8 @@ module.exports = {
   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"',
-  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/dev/"'
+  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/dev/"',
+  FWZL_PAGE_URL: '"http://8.134.39.104:7492/project?project_code={projectCode}"',
+  CPZL_PAGE_URL: '"http://8.134.39.104:7492/project?project_code={projectCode}"',
+  RYPG_PAGE_URL: '"http://8.134.39.104:7477/#/evaofproj/{projectCode}"'
 }

+ 4 - 1
config/prod.env.js

@@ -5,5 +5,8 @@ module.exports = {
   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"',
-  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/online/"'
+  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/online/"',
+  FWZL_PAGE_URL: '"http://8.134.39.104:7492/project?project_code={projectCode}"',
+  CPZL_PAGE_URL: '"http://8.134.39.104:7492/project?project_code={projectCode}"',
+  RYPG_PAGE_URL: '"http://8.134.39.104:7477/#/evaofproj/{projectCode}"'
 }

+ 4 - 1
config/test.env.js

@@ -12,5 +12,8 @@ module.exports = {
   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/"'
+  OSS_URL: '"https://mooctest-crowd-service.oss-cn-hangzhou.aliyuncs.com/Plantform/dev/"',
+  FWZL_PAGE_URL: '"http://8.134.39.104:7492/project?project_code={projectCode}"',
+  CPZL_PAGE_URL: '"http://8.134.39.104:7492/project?project_code={projectCode}"',
+  RYPG_PAGE_URL: '"http://8.134.39.104:7477/#/evaofproj/{projectCode}"'
 }

+ 83 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "echarts-wordcloud": "^2.0.0",
         "element-ui": "^2.14.1",
         "font-awesome": "^4.7.0",
+        "html2canvas": "^1.4.1",
         "mockjs": "^1.1.0",
         "moment": "^2.29.1",
         "querystring": "^0.2.0",
@@ -4287,6 +4288,14 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/base64-js": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
@@ -5537,6 +5546,14 @@
         "node": ">=6"
       }
     },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/css-loader": {
       "version": "3.6.0",
       "resolved": "https://registry.npm.taobao.org/css-loader/download/css-loader-3.6.0.tgz?cache=0&sync_timestamp=1604507120816&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcss-loader%2Fdownload%2Fcss-loader-3.6.0.tgz",
@@ -9211,6 +9228,18 @@
         "object-assign": "^4.0.1"
       }
     },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/htmlparser2": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -16062,6 +16091,14 @@
         "inherits": "2"
       }
     },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -16811,6 +16848,14 @@
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "node_modules/uuid": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
@@ -21567,6 +21612,11 @@
         }
       }
     },
+    "base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
+    },
     "base64-js": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
@@ -22661,6 +22711,14 @@
         }
       }
     },
+    "css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
     "css-loader": {
       "version": "3.6.0",
       "resolved": "https://registry.npm.taobao.org/css-loader/download/css-loader-3.6.0.tgz?cache=0&sync_timestamp=1604507120816&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcss-loader%2Fdownload%2Fcss-loader-3.6.0.tgz",
@@ -25740,6 +25798,15 @@
         }
       }
     },
+    "html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "requires": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      }
+    },
     "htmlparser2": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -31407,6 +31474,14 @@
         "inherits": "2"
       }
     },
+    "text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -32031,6 +32106,14 @@
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
       "dev": true
     },
+    "utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "requires": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "uuid": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "echarts-wordcloud": "^2.0.0",
     "element-ui": "^2.14.1",
     "font-awesome": "^4.7.0",
+    "html2canvas": "^1.4.1",
     "mockjs": "^1.1.0",
     "moment": "^2.29.1",
     "querystring": "^0.2.0",

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

@@ -52,7 +52,7 @@ export default {
           this.fileList = []
           this.files.map(file => {
             let fileName = file.substring(file.lastIndexOf('/') + 1)
-            fileName = fileName.substring(0, fileName.indexOf('_')) + fileName.substring(fileName.lastIndexOf('.'))
+            fileName = fileName.substring(0, fileName.lastIndexOf('_', fileName.lastIndexOf('_') - 1)) + fileName.substring(fileName.lastIndexOf('.'))
             this.fileList.push({ name: fileName, url: file })
           })
         }

+ 1 - 1
src/components/file/ImgUpload.vue

@@ -47,7 +47,7 @@ export default {
           this.fileList = []
           this.files.map(file => {
             this.fileList.push({ url: file })
-          })
+            })
         }
       }
     }

+ 83 - 12
src/components/project/Project.vue

@@ -233,15 +233,24 @@
               <el-button v-if="projectOperationControl.uploadReport" type="primary" size="mini" @click="createReport()">
                 上传报告
               </el-button>
-              <el-button v-if="this.project.status==4" type="primary" size="mini" @click="qualityEvaluate()">
+              <el-button v-if="this.project.status==4 && this.cpzlStatus" type="primary" size="mini" @click="qualityEvaluate()">
                 产品质量评估
               </el-button>
-              <el-button v-if="this.project.status==4"  type="primary" size="mini" @click="memberEvaluate()">
+              <el-button v-if="this.project.status==4 && !this.cpzlStatus" type="success" size="mini" @click="generateQualityEvaluate()">
+                产品质量评估生成
+              </el-button>
+              <el-button v-if="this.project.status==4 && this.rypgStatus"  type="primary" size="mini" @click="memberEvaluate()">
                 人员评估
               </el-button>
-              <el-button v-if="this.project.status==4"  type="primary" size="mini" @click="serviceEvaluate()">
+              <el-button v-if="this.project.status==4 && !this.rypgStatus"  type="success" size="mini" @click="generateMemberEvaluate()">
+                人员评估生成
+              </el-button>
+              <el-button v-if="this.project.status==4 && this.fwzlStatus"  type="primary" size="mini" @click="serviceEvaluate()">
                 服务质量评估
               </el-button>
+              <el-button v-if="this.project.status==4 && !this.fwzlStatus"  type="success" size="mini" @click="generateServiceEvaluate()">
+                服务质量评估生成
+              </el-button>
             </el-form-item>
           </el-form>
 
@@ -333,6 +342,8 @@
     storageGet,
     submitProjectRequest
   } from '@/js/index'
+  import http from '../../js/http'
+  import api from '../../js/api'
 
   export default {
     name: 'Project',
@@ -429,6 +440,9 @@
             }
           ]
         },
+        cpzlStatus: true,
+        rypgStatus: true,
+        fwzlStatus: true,
         rules: {
           name: [
             {required: true, message: '请输入项目名称', trigger: 'blur'},
@@ -562,6 +576,21 @@
         this.setInstitutions()
         this.setUserInfo()
         this.loadData()
+        http.get(api.FWZL.STATUS.replace('{projectCode}', this.projectId)).then((res) => {
+          this.fwzlStatus = true && res.data
+        }).catch(error => {
+          notify('error', '获取服务质量评估状态异常:' + error.data)
+        })
+        http.get(api.CPZL.STATUS.replace('{projectCode}', this.projectId)).then((res) => {
+          this.cpzlStatus = true && res.data
+        }).catch(error => {
+          notify('error', '获取产品质量评估状态异常:' + error.data)
+        })
+        http.get(api.RYPG.STATUS.replace('{projectCode}', this.projectId)).then((res) => {
+          this.rypgStatus = true && res.data
+        }).catch(error => {
+          notify('error', '获取人员评估状态异常:' + error.data)
+        })
         //this.reformDate(123)
         // this.project.platform.map(item => {
         //   this.platformType.push(PlatformType[item])
@@ -833,17 +862,59 @@
           }
         })
       },
-      qualityEvaluate(){
-        let qualityUrl='http://8.134.39.104:8080/nfsplatform/twpevaluate/skipAssess?project_code='+this.projectId;
-        window.open(qualityUrl, '_blank');
+      qualityEvaluate () {
+        let qualityUrl = process.env.CPZL_PAGE_URL.replace('{projectCode}', this.projectId)
+        window.open(qualityUrl, '_blank')
+      },
+      generateQualityEvaluate () {
+        http.post(api.CPZL.GENERATE.replace('{projectCode}', this.projectId), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '产品质量评估结果生成中...')
+            this.cpzlStatus = true
+          } else {
+            notify('error', '产品质量评估结果生成失败')
+            this.cpzlStatus = false
+          }
+        }).catch((error) => {
+          notify('error', '产品质量评估生成异常:' + error.data)
+          this.cpzlStatus = false
+        })
       },
-      memberEvaluate(){
-        let memberUrl='http://8.134.39.104:7477/index.html#/index/evaluatetester/testeroftask/'+this.projectId;
-        window.open(memberUrl, '_blank');
+      memberEvaluate () {
+        let qualityUrl = process.env.RYPG_PAGE_URL.replace('{projectCode}', this.projectId)
+        window.open(qualityUrl, '_blank')
       },
-      serviceEvaluate(){
-        let serviceUrl='http://8.134.39.104:7492/'+this.projectId;
-        window.open(serviceUrl, '_blank');
+      generateMemberEvaluate () {
+        http.post(api.RYPG.GENERATE.replace('{projectCode}', this.projectId), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '人员评估结果生成中...')
+            this.rypgStatus = true
+          } else {
+            notify('error', '人员评估结果生成失败')
+            this.rypgStatus = false
+          }
+        }).catch((error) => {
+          notify('error', '人员评估生成异常:' + error.data)
+          this.rypgStatus = false
+        })
+      },
+      serviceEvaluate () {
+        let qualityUrl = process.env.FWZL_PAGE_URL.replace('{projectCode}', this.projectId)
+        window.open(qualityUrl, '_blank')
+      },
+      generateServiceEvaluate () {
+        http.post(api.FWZL.GENERATE.replace('{projectCode}', this.projectId), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '服务质量评估结果生成中...')
+            this.fwzlStatus = true
+          } else {
+            notify('error', '服务质量评估结果生成失败')
+            this.fwzlStatus = false
+          }
+        }).catch((error) => {
+          notify('error', '服务质量评估生成异常:' + error.data)
+          this.fwzlStatus = false
+        })
       },
       handleDelete(index, id) {
         this.$confirm('确认删除该任务?')

+ 18 - 0
src/components/task/Task.vue

@@ -231,6 +231,12 @@
               <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="exportReport()">
+                导出原始报告
+              </el-button>
+              <el-button v-if="taskOperationControl.testCaseExam" type="primary" size="mini" @click="toMixDefectPage()">
+                报告融合
+              </el-button>
               <el-button v-if="taskOperationControl.testCaseExam" type="primary" size="mini" @click="toTestCasesExam()">
                 众测审核
               </el-button>
@@ -1008,6 +1014,18 @@ export default {
         }
       })
     },
+    exportReport () {
+      window.location.href = Apis.TESTCASE.EXPORT.replace('{taskCode}', this.taskId)
+    },
+    toMixDefectPage () {
+      this.$router.push({
+        name: 'MixDefectPage',
+        params: {
+          projectCode: this.projectId,
+          taskCode: this.taskId
+        }
+      })
+    },
     // 跳转到测试用例审核页面
     toTestCasesExam () {
       console.log(this.user.userVO.id)

+ 36 - 3
src/js/api.js

@@ -30,7 +30,7 @@ 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_USER_TASK: '/api/usertask/{taskCode}/commit',
     TASK_STATISTICS: '/api/task/{taskCode}/statistics',
     TASK_COMPLETION: '/api/task/{taskCode}/completion'
   },
@@ -122,7 +122,9 @@ export default {
     DELETE_DEFECT: '/api/testcase/defect/{id}/',
     EXAM: '/api/testcase/exam/{id}/',
     UPLOAD_TEST_CASES_FILE: '/api/testcase/upload/{taskCode}/',
-    UPLOAD_DEFECTS_FILE: '/api/testcase/uploaddefects/{taskCode}/'
+    UPLOAD_DEFECTS_FILE: '/api/testcase/uploaddefects/{taskCode}/',
+    EXAM_ALL_VALID: '/api/testcase/examallvalid/{taskCode}/{designerId}/',
+    EXPORT: '/api/testcase/export/{taskCode}'
   },
   TESTENV: {
     ADD: '/api/testenv/',
@@ -139,5 +141,36 @@ export default {
   TESTER: {
     TESTER_TASK_INFO: '/api/tester/task/{userId}/{taskCode}/'
   },
-  LOGIN: '/api/login2/'
+  LOGIN: '/api/login2/',
+  STATISTICS: '/api/statistics',
+  TESTER_CERT: {
+    GET_USER_CERT: '/api/testercert/'
+  },
+  QRCODE: '/api/qrcode/{content}',
+  MIXDEFECT: {
+    LIST: '/api/mixdefect/{taskCode}',
+    EXCHANGE: '/api/mixdefect/exchange/{masterDefectId}/{suppDefectId}',
+    TOMASTER: '/api/mixdefect/tomaster/{suppDefectId}',
+    TOOTHERMASTERDEFECT: '/api/mixdefect/toothermasterdefect/{masterDefectId}/{movedDefectId}',
+    EXPORT: '/api/mixdefect/export/{taskCode}',
+    STATUS: '/api/mixdefect/status/{taskCode}',
+    GENERATE: '/api/mixdefect/generate/{taskCode}',
+    EXPORT_PROJECT: '/api/mixdefect/exportproject/{projectCode}'
+  },
+  FWZL: {
+    STATUS: '/api/fwzl/status/{projectCode}',
+    GENERATE: '/api/fwzl/generate/{projectCode}'
+  },
+  CPZL: {
+    STATUS: '/api/cpzl/status/{projectCode}',
+    GENERATE: '/api/cpzl/generate/{projectCode}'
+  },
+  RYPG: {
+    STATUS: '/api/rypg/status/{projectCode}',
+    GENERATE: '/api/rypg/generate/{projectCode}'
+  },
+  TASKAMOUNT: {
+    CAL: '/api/taskamount/calculation/{taskCode}/{effectiveWorkloadAmount}/{defectExciationAmount}/{extraRewardAmount}',
+    CAL_AND_END: '/api/taskamount/calandend'
+  }
 }

+ 268 - 0
src/pages/Amount/TaskAmountCal.vue

@@ -0,0 +1,268 @@
+<template>
+  <div>
+    <div style="padding-left: 30px;padding-top: 30px;padding-bottom: 30px; font-weight: 600;">
+      <span style="margin-left: 20px;">有效测试用例总数:{{taskAmount.totalEffectiveTestCaseCount}}</span>
+      <span style="margin-left: 20px;">缺陷总评分:{{taskAmount.totalDefectScore}}</span>
+      <span style="margin-left: 20px;">任务总金额:{{taskAmount.totalAmount}}</span>
+    </div>
+    <div>
+      <el-form :inline="true" :model="taskAmount" :rules="rules" label-position="left" label-width="100px" style="width: 100%; margin-left:50px;">
+        <el-form-item label="有效测试用例奖励金额" prop="effectiveWorkloadAmount" label-width="auto">
+          <el-input v-model.number="taskAmount.effectiveWorkloadAmount"/>
+        </el-form-item>
+        <el-form-item label="缺陷奖励金额" prop="defectExciationAmount" label-width="auto">
+          <el-input v-model.number="taskAmount.defectExciationAmount"/>
+        </el-form-item>
+        <el-form-item label="额外奖励金额" prop="extraRewardAmount" label-width="auto">
+          <el-input v-model.number="taskAmount.extraRewardAmount"/>
+        </el-form-item>
+        <el-form-item prop="extraRewardAmount">
+          <el-button type="primary" @click="cal">计算</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div>
+      <el-table
+        :data="taskAmount.testerAmounts"
+        border
+        fit
+        highlight-current-row
+        style="width: 100%; "
+        max-height="500"
+      >
+        <el-table-column type="expand" align="center" min-width="1%">
+          <template slot-scope="{row, $index}">
+            <el-table
+              :data="[row]"
+              border
+              fit
+              highlight-current-row
+              style="width: 100%; "
+              max-height="500"
+            >
+              <el-table-column label="独有的致命缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.onlyOneVeryHighDefectCount }}</div>
+                  <div>得分:{{ row.onlyOneVeryHighDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="独有的严重缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.onlyOneHighDefectCount }}</div>
+                  <div>得分:{{ row.onlyOneHighDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="独有的一般缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.onlyOneMidDefectCount }}</div>
+                  <div>得分:{{ row.onlyOneMidDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="独有的轻微缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.onlyOneLowDefectCount }}</div>
+                  <div>得分:{{ row.onlyOneLowDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="独有的建议缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.onlyOneVeryLowDefectCount }}</div>
+                  <div>得分:{{ row.onlyOneVeryLowDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="优质的致命缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.topVeryHighDefectCount }}</div>
+                  <div>得分:{{ row.topVeryHighDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="优质的严重缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.topHighDefectCount }}</div>
+                  <div>得分:{{ row.topHighDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="优质的一般缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.topMidDefectCount }}</div>
+                  <div>得分:{{ row.topMidDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="优质的轻微缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.topLowDefectCount }}</div>
+                  <div>得分:{{ row.topLowDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="优质的建议缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.topVeryLowDefectCount }}</div>
+                  <div>得分:{{ row.topVeryLowDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="补充的致命缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.notTopVeryHighDefectCount }}</div>
+                  <div>得分:{{ row.notTopVeryHighDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="补充的严重缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.notTopHighDefectCount }}</div>
+                  <div>得分:{{ row.notTopHighDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="补充的一般缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.notTopMidDefectCount }}</div>
+                  <div>得分:{{ row.notTopMidDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="补充的轻微缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.notTopLowDefectCount }}</div>
+                  <div>得分:{{ row.notTopLowDefectScore }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="补充的建议缺陷" prop="code" align="center" min-width="3%">
+                <template slot-scope="{row}">
+                  <div>数量:{{ row.notTopVeryLowDefectCount }}</div>
+                  <div>得分:{{ row.notTopVeryLowDefectScore }}</div>
+                </template>
+              </el-table-column>
+            </el-table>
+          </template>
+        </el-table-column>
+        <el-table-column label="姓名" prop="code" align="center" min-width="3%">
+          <template slot-scope="{row}">
+            <span>{{ row.testerRealName }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="有效测试用例数" align="center" :show-overflow-tooltip="true" min-width="6%">
+          <template slot-scope="{row}">
+            <span>{{ row.effectiveTestCaseCount }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="缺陷评分" align="center" :show-overflow-tooltip="true" min-width="6%">
+          <template slot-scope="{row}">
+            <span>{{ row.defectScore }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="有效测试用例奖励金额" align="center" :show-overflow-tooltip="true" min-width="6%">
+          <template slot-scope="{row}">
+            <span>{{ row.effectiveWorkloadAmount }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="缺陷激励金额" align="center" :show-overflow-tooltip="true" min-width="6%">
+          <template slot-scope="{row}">
+            <span>{{ row.defectExciationAmount }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="额外奖励金额" align="center" :show-overflow-tooltip="true" min-width="6%">
+          <template slot-scope="{row}">
+            <el-input v-model.number="row.extraRewardAmount"/>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <div style="width: 100%;  margin-top: 20px;">
+      <el-button :disabled="!canSubmit" style="display: block; margin-left: auto; margin-right: auto;" type="primary" @click="submit()">提交并结束任务</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+
+export default {
+  name: 'TaskAmountCal',
+  data: function () {
+    return {
+      taskAmount: {
+        effectiveWorkloadAmount: 0.0,
+        defectExciationAmount: 0.0,
+        extraRewardAmount: 0.0
+      },
+      rules: {
+        effectiveWorkloadAmount: [
+          {required: true, message: '有效测试用例奖励金额不能为空', trigger: 'blur'}
+        ],
+        defectExciationAmount: [
+          {required: true, message: '缺陷奖励金额不能为空', trigger: 'blur'}
+        ],
+        extraRewardAmount: [
+          {required: true, message: '额外奖励金额不能为空', trigger: 'blur'}
+        ]
+      },
+      canSubmit: false
+    }
+  },
+  mounted () {
+    this.cal()
+  },
+  methods: {
+    cal () {
+      if (isNaN(this.taskAmount.effectiveWorkloadAmount)) {
+        notify('error', '有效测试用例奖励金额必须为数字!')
+        return
+      }
+      if (isNaN(this.taskAmount.defectExciationAmount)) {
+        notify('error', '缺陷奖励金额必须为数字!')
+        return
+      }
+      if (isNaN(this.taskAmount.extraRewardAmount)) {
+        notify('error', '额外奖励金额必须为数字!')
+        return
+      }
+      Http.get(Api.TASKAMOUNT.CAL.replace('{taskCode}', this.$route.params.taskCode)
+        .replace('{effectiveWorkloadAmount}', this.taskAmount.effectiveWorkloadAmount)
+        .replace('{defectExciationAmount}', this.taskAmount.defectExciationAmount)
+        .replace('{extraRewardAmount}', this.taskAmount.extraRewardAmount)).then((res) => {
+        this.taskAmount = res.data
+        this.canSubmit = this.taskAmount.status === 3
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取任务金额数据失败:系统异常')
+      })
+    },
+    submit () {
+      let postData = {}
+      if ((this.taskAmount.effectiveWorkloadAmount + this.taskAmount.defectExciationAmount + this.taskAmount.extraRewardAmount) !== this.taskAmount.totalAmount) {
+        notify('error', '有效测试用例奖励金额+缺陷奖励金额+额外奖励金额需等于任务总金额!')
+        return
+      }
+      postData['taskCode'] = this.$route.params.taskCode
+      postData['effectiveWorkloadAmount'] = this.taskAmount.effectiveWorkloadAmount
+      postData['defectExciationAmount'] = this.taskAmount.defectExciationAmount
+      postData['extraRewardAmount'] = this.taskAmount.extraRewardAmount
+      let testerExtraRewardAmounts = []
+      let totalExtraRewardAmount = 0.0
+      this.taskAmount.testerAmounts.forEach(testerAmount => {
+        totalExtraRewardAmount += testerAmount.extraRewardAmount
+        testerExtraRewardAmounts.push({'userId': testerAmount.testerId, 'amount': testerAmount.extraRewardAmount})
+      })
+      if (totalExtraRewardAmount !== this.taskAmount.extraRewardAmount) {
+        notify('error', '所有测试人员的额外奖励金额加起来需等于总的额外奖励金额!')
+        return
+      }
+      postData['testerExtraRewardAmounts'] = testerExtraRewardAmounts
+      Http.post(Api.TASKAMOUNT.CAL_AND_END, postData).then((res) => {
+        if (res.code === 20000) {
+          notify('success', '提交成功')
+        } else {
+          notify('error', '提交失败')
+        }
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '提交异常:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 93 - 0
src/pages/Statistics/Statistics.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="dashboard-editor-container" v-loading="loading">
+    <el-row style="background:transparent;margin-bottom:32px;height:1200px;width: 100%;">
+      <el-col :xs="24" :sm="24" :lg="18" style="height: 100%;">
+        <el-row style="height: 40%;width: 100%;">
+          <el-col :xs="24" :sm="24" :lg="11" style="height: 100%;">
+            <task-month-count-chart :year-month-counts="statisticsData.yearMonthCounts"/>
+          </el-col>
+          <el-col :xs="24" :sm="24" :lg="13" style="height: 100%;">
+            <count :counts="statisticsData"/>
+          </el-col>
+        </el-row>
+        <el-row style="height: 60%;width: 100%;">
+          <el-col :xs="24" :sm="24" :lg="10" style="height: 100%;">
+            <el-row style="height: 55%;width: 100%;">
+              <el-col :xs="24" :sm="24" :lg="24" style="height: 100%;">
+                <project-type-count-chart :counts="statisticsData.projectTypeCounts"/>
+              </el-col>
+            </el-row>
+            <el-row style="height: 45%;width: 100%; ">
+              <el-col :xs="24" :sm="24" :lg="24" style="height: 100%;">
+                <app-type-chart :app-type-counts="statisticsData.appTypeCounts"/>
+              </el-col>
+            </el-row>
+          </el-col>
+          <el-col :xs="24" :sm="24" :lg="14" style="height: 100%;">
+            <total-map :province-counts="statisticsData.provinceStatisticses"/>
+          </el-col>
+        </el-row>
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="6" style="height: 100%;">
+        <el-row style="height: 60%;width: 100%;">
+          <el-col :xs="24" :sm="24" :lg="24" style="height: 100%;">
+            <province-table :province-counts="statisticsData.provinceStatisticses"/>
+          </el-col>
+        </el-row>
+        <el-row style="height: 40%;width: 100%;">
+          <el-col :xs="24" :sm="24" :lg="24" style="height: 100%;">
+            <project-field-chart :project-field-counts="statisticsData.projectFieldCounts"/>
+          </el-col>
+        </el-row>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import Count from './components/Count'
+import TaskMonthCountChart from './components/TaskMonthCountChart'
+import ProjectTypeCountChart from './components/ProjectTypeCountChart'
+import AppTypeChart from './components/AppTypeChart'
+import ProjectFieldChart from './components/ProjectFieldChart'
+import ProvinceTable from './components/ProvinceTable'
+import TotalMap from './components/map/TotalMap'
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+
+export default {
+  name: 'Statistics',
+  components: {Count, TaskMonthCountChart, ProjectTypeCountChart, AppTypeChart, ProjectFieldChart, ProvinceTable, TotalMap},
+  data: function () {
+    return {
+      statisticsData: {},
+      loading: true
+    }
+  },
+  mounted () {
+    this.$nextTick(() => this.getStatisticsData())
+  },
+  methods: {
+    getStatisticsData () {
+      this.loading = true
+      Http.get(Api.STATISTICS).then((res) => {
+        this.statisticsData = res.data
+        this.loading = false
+      }).catch((error) => {
+        this.loading = false
+        notify('error', '获取统计数据失败:系统异常')
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dashboard-editor-container {
+  padding: 20px;
+  background-color: #1e2436;
+  position: relative;
+  color: #dddddd;
+}
+</style>

+ 3 - 3
src/pages/Statistics/TaskStatistics.vue

@@ -7,7 +7,7 @@
     <project-search :firstSelectedProjectCode="selectedProjectCode"></project-search>
     <task-search :firstSelectedTaskCode="selectedTaskCode" :selectedCallback="getTaskData"></task-search>
 
-    <panel-group :info="taskInfo" :task-code="selectedTaskCode"/>
+    <panel-group :info="taskInfo" :taskCode="selectedTaskCode"/>
 
     <el-row :gutter="20" style="background:transparent;margin-bottom:32px;height: 650px">
       <el-col :xs="24" :sm="24" :lg="5" style="height: 100%">
@@ -60,7 +60,7 @@ 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 StaffDistribution from './components/map/StaffDistributionMap'
 import ProjectSearch from '@/components/project/ProjectSearch'
 import TaskSearch from '@/components/project/TaskSearch'
 import Api from '@/js/api'
@@ -93,7 +93,7 @@ export default {
       loading: true,
       progress: 0,
       selectedProjectCode: this.$route.params.projectCode,
-      selectedTaskCode: this.$route.params.taskCode,
+      selectedTaskCode: this.$route.params.taskCode
     }
   },
   methods: {

+ 103 - 0
src/pages/Statistics/components/AppTypeChart.vue

@@ -0,0 +1,103 @@
+<template>
+  <div style="height: 100%">
+    <div class="chart-title">众测项目应用统计</div>
+    <div id='appTypeChart' :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 {
+    name: 'AppTypeChart',
+    mixins: [resize],
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '90%'
+      },
+      height: {
+        type: String,
+        default: '90%'
+      },
+      appTypeCounts: {
+        type: Object,
+        default: function () {
+          return {}
+        }
+      }
+    },
+    watch: {
+      appTypeCounts: {
+        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('appTypeChart'), 'macarons')
+        this.chart.setOption({
+          legend: {
+            orient: 'vertical',
+            left: 'left',
+            textStyle: {
+              color: '#dddddd'
+            }
+          },
+          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: 55px 8px 15px 8px;
+  font-weight: bold;
+  font-size: 18px;
+}
+</style>

+ 164 - 0
src/pages/Statistics/components/Count.vue

@@ -0,0 +1,164 @@
+<template>
+  <div style="text-align:left; color: #dddddd; padding-right: 40px; padding-top: 40px; padding-bottom: 40px;">
+    <el-row style="height: 30%;width: 100%;">
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">众测工人</p>
+        <p class="title2">
+          <label>{{countDatas.testerCount}}</label>
+          <label>人</label>
+        </p>
+      </el-col>
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">注册企业</p>
+        <p class="title2">
+          <label>{{countDatas.companyCount}}</label>
+          <label>家</label>
+        </p>
+      </el-col>
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">注册机构</p>
+        <p class="title2">
+          <label>{{countDatas.orgCount}}</label>
+          <label>家</label>
+        </p>
+      </el-col>
+    </el-row>
+    <el-row style="height: 40%;width: 100%;">
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">众测项目</p>
+        <p class="title3">
+          <label>累计</label>
+          <label class="title3-color1">{{countDatas.projectCount}}</label>
+          <label>项</label>
+        </p>
+        <p class="title3">
+          <label>当月新增</label>
+          <label class="title3-color1">{{countDatas.newProjectCount}}</label>
+          <label>项</label>
+        </p>
+      </el-col>
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">众测任务</p>
+        <p class="title3">
+          <label>累计</label>
+          <label class="title3-color1">{{countDatas.taskCount}}</label>
+          <label>项</label>
+        </p>
+        <p class="title3">
+          <label>当月新增</label>
+          <label class="title3-color1">{{countDatas.newTaskCount}}</label>
+          <label>项</label>
+        </p>
+      </el-col>
+      <el-col :offset="2" :xs="24" :sm="24" :lg="6" style="height: 100%;">
+        <p class="title1">参与众测人次</p>
+        <p class="title3">
+          <label>累计</label>
+          <label class="title3-color1">{{countDatas.testerJoinCount}}</label>
+          <label>人次</label>
+        </p>
+        <p class="title3">
+          <label>当月新增</label>
+          <label class="title3-color1">{{countDatas.newTesterJoinCount}}</label>
+          <label>人次</label>
+        </p>
+      </el-col>
+    </el-row>
+    <el-row style="height: 40%;width: 100%;">
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">提交测试用例</p>
+        <p class="title3">
+          <label>累计</label>
+          <label class="title3-color2">{{countDatas.testCaseCount}}</label>
+          <label>个</label>
+        </p>
+        <p class="title3">
+          <label>当月新增</label>
+          <label class="title3-color2">{{countDatas.newTestCaseCount}}</label>
+          <label>个</label>
+        </p>
+      </el-col>
+      <el-col :offset="2" :xs="24" :sm="24" :lg="5" style="height: 100%;">
+        <p class="title1">提交缺陷</p>
+        <p class="title3">
+          <label>累计</label>
+          <label class="title3-color2">{{countDatas.defectCount}}</label>
+          <label>个</label>
+        </p>
+        <p class="title3">
+          <label>当月新增</label>
+          <label class="title3-color2">{{countDatas.newDefectCount}}</label>
+          <label>个</label>
+        </p>
+      </el-col>
+      <el-col :offset="2" :xs="24" :sm="24" :lg="6" style="height: 100%;">
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Count',
+  data: function () {
+    return {
+      countDatas: {}
+    }
+  },
+  props: {
+    counts: {
+      type: Object,
+      required: true
+    }
+  },
+  watch: {
+    counts: {
+      handler: function (nv, ov) {
+        this.countDatas = nv
+      },
+      deep: true,
+      immediate: true
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.title1 {
+  font-weight: bold;
+  font-size: 14px;
+}
+.title2 {
+  label:nth-child(1) {
+    margin-left: 8px;
+    font-weight: bold;
+    font-size: 24px;
+    color: #4dd9d5;
+  }
+  label:nth-child(2) {
+    font-weight: bold;
+    font-size: 6px;
+  }
+}
+.title3 {
+  label:nth-child(1) {
+    font-weight: bold;
+    font-size: 10px;
+  }
+  label:nth-child(2) {
+    margin-left: 10px;
+    font-weight: bold;
+    font-size: 24px;
+  }
+  label:nth-child(3) {
+    font-weight: bold;
+    font-size: 6px;
+  }
+  .title3-color1 {
+    color: red;
+  }
+  .title3-color2 {
+    color: green;
+  }
+}
+</style>

+ 3 - 3
src/pages/Statistics/components/PanelGroup.vue

@@ -85,7 +85,6 @@ import Http from '@/js/http'
 import {notify} from '@/constants'
 
 export default {
-  props: ['info'],
   components: {
     CountTo
   },
@@ -94,10 +93,11 @@ export default {
       completion: 0
     }
   },
-  prop: {
+  props: {
+    info: Object,
     taskCode: String
   },
-  created () {
+  created: function () {
     this.getCompletion()
   },
   methods: {

+ 104 - 0
src/pages/Statistics/components/ProjectFieldChart.vue

@@ -0,0 +1,104 @@
+<template>
+  <div style="height: 100%">
+    <div class="chart-title">众测项目领域分布</div>
+    <div id='projectFieldChart' :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 {
+    name: 'ProjectFieldChart',
+    mixins: [resize],
+    props: {
+      className: {
+        type: String,
+        default: 'chart'
+      },
+      width: {
+        type: String,
+        default: '85%'
+      },
+      height: {
+        type: String,
+        default: '90%'
+      },
+      projectFieldCounts: {
+        type: Object,
+        default: function () {
+          return {}
+        }
+      }
+    },
+    watch: {
+      projectFieldCounts: {
+        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: [{'name': '科技服务', 'value': 10}, {'name': '服务互联网', 'value': 20}, {'name': '集成电路', 'value': 15},
+         {'name': '智能传感器', 'value': 11}, {'name': '高端设备', 'value': 4}, {'name': '其他', 'value': 8}]
+      }
+    },
+    mounted () {
+      this.$nextTick(() => {
+        this.initChart()
+      })
+    },
+    beforeDestroy () {
+      if (!this.chart) {
+        return
+      }
+      this.chart.dispose()
+      this.chart = null
+    },
+    methods:{
+      initChart () {
+        this.chart = echarts.init(document.getElementById('projectFieldChart'), 'macarons')
+        this.chart.setOption({
+          legend: {
+            orient: 'vertical',
+            left: 'right',
+            textStyle: {
+              color: '#dddddd'
+            }
+          },
+          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: 55px 8px 15px 8px;
+  font-weight: bold;
+  font-size: 18px;
+}
+</style>

+ 137 - 0
src/pages/Statistics/components/ProjectTypeCountChart.vue

@@ -0,0 +1,137 @@
+<template>
+  <div style="height: 100%;padding-left: 0px;">
+    <div class="chart-title">众测项目类型统计</div>
+    <div id="projectTypeCountChart" :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 {
+  name: 'ProjectTypeCountChart',
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '90%'
+    },
+    counts: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  watch: {
+    counts: {
+      handler (nv, ov) {
+        this.projectTypes = []
+        this.projectTypeCounts = []
+        const types = Object.keys(nv)
+        types.forEach(type => {
+          if (this.projectTypeCountMap[type] === undefined) {
+            this.projectTypeCountMap['其它'] = this.projectTypeCountMap['其它'] + nv[type]
+          } else {
+            this.projectTypeCountMap[type] = this.projectTypeCountMap[type] + nv[type]
+          }
+        })
+        const finalTypes = Object.keys(this.projectTypeCountMap)
+        finalTypes.forEach(type => {
+          this.projectTypes.push(type)
+          this.projectTypeCounts.push(this.projectTypeCountMap[type])
+        })
+        this.initChart()
+      },
+      deep: true
+    }
+  },
+  data () {
+    return {
+      chart: null,
+      projectTypeCountMap: {
+        '其它': 0,
+        '易用性测试': 0,
+        '可靠性测试': 0,
+        '风险评估': 0,
+        '代码安全审计': 0,
+        '兼容性测试': 0,
+        '安全测试': 0,
+        '功能测试': 0,
+        '性能测试': 0
+      },
+      projectTypes: [],
+      projectTypeCounts: []
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy () {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart () {
+      this.chart = echarts.init(document.getElementById('projectTypeCountChart'), 'macarons')
+      this.chart.setOption({
+        textStyle: {
+          color: '#dddddd'
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          top: 10,
+          left: '2%',
+          right: '2%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: [{
+          name: '数量',
+          type: 'value'
+        }],
+        yAxis: [{
+          name: '项目类型',
+          type: 'category',
+          data: Array.from(this.projectTypes)
+        }],
+        series: [{
+          type: 'bar',
+          barWidth: '60%',
+          data: Array.from(this.projectTypeCounts),
+          animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.chart-title{
+  padding: 5px 8px 15px 8px;
+  font-weight: bold;
+  font-size: 18px;
+}
+</style>

+ 82 - 0
src/pages/Statistics/components/ProvinceTable.vue

@@ -0,0 +1,82 @@
+<template>
+  <div style="height: 90%;">
+    <div class="chart-title">众测平台使用分析</div>
+    <el-table :data="provinceCountDatas" :cell-style="{'background-color': '#3e4f6f', borderColor:'#1e2436', borderWidth:'2px', padding: '2px'}" :header-cell-style="{'background-color': '#3e4f6f', borderColor:'#1e2436', borderWidth:'2px', padding: '2px'}" width="100%" height="100%" style="background-color: #3e4f6f; color: #888888; font-size: 12px;">
+      <el-table-column fixed label="省份" min-width="20%" 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="15%" align="center">
+        <template slot-scope="scope" class="no-wrap">
+          {{ scope.row.testerCount }}
+        </template>
+      </el-table-column>
+      <el-table-column label="服务企业" min-width="15%" align="center">
+        <template slot-scope="scope" class="no-wrap">
+          {{ scope.row.companyCount }}
+        </template>
+      </el-table-column>
+      <el-table-column label="众测机构" min-width="15%" align="center">
+        <template slot-scope="scope" class="no-wrap">
+          {{ scope.row.orgCount }}
+        </template>
+      </el-table-column>
+      <el-table-column label="项目发包" min-width="15%" align="center">
+        <template slot-scope="scope" class="no-wrap">
+          {{ scope.row.projectCount }}
+        </template>
+      </el-table-column>
+      <el-table-column label="任务发包" min-width="15%" align="center">
+        <template slot-scope="scope" class="no-wrap">
+          {{ scope.row.taskCount }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+  // import { transactionList } from '@/api/remote-search'
+
+  export default {
+    props: {
+      provinceCounts: {
+        type: Array
+      }
+    },
+    watch: {
+      provinceCounts: {
+        handler (nv, ov) {
+          this.provinceCountDatas = nv
+        },
+        deep: true
+      }
+    },
+    data () {
+      return {
+        provinceCountDatas: []
+      }
+    }
+  }
+</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;
+}
+
+.chart-title{
+  padding: 55px 8px 15px 8px;
+  font-weight: bold;
+  font-size: 18px;
+}
+</style>

+ 115 - 0
src/pages/Statistics/components/TaskMonthCountChart.vue

@@ -0,0 +1,115 @@
+<template>
+  <div style="height: 100%;">
+    <div class="chart-title">每月接发任务数量统计</div>
+    <div id='taskMonthCountChart' :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')
+
+export default {
+  name: 'TaskMonthCountChart',
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '90%'
+    },
+    height: {
+      type: String,
+      default: '90%'
+    },
+    yearMonthCounts: {
+      type: Array
+    }
+  },
+  data () {
+    return {
+      yearMonths: [],
+      sendTaskCounts: [],
+      recvTaskCounts: []
+    }
+  },
+  watch: {
+    yearMonthCounts: {
+      handler (nv, ov) {
+        this.yearMonths = []
+        this.sendTaskCounts = []
+        this.recvTaskCounts = []
+        nv.forEach(yearMonthCountData => {
+          this.yearMonths.push(yearMonthCountData['yearMonth'])
+          this.sendTaskCounts.push(yearMonthCountData['sendTaskCount'])
+          this.recvTaskCounts.push(yearMonthCountData['recvTaskCount'])
+        })
+        this.initChart()
+      }
+    }
+  },
+  mounted () {
+    this.$nextTick(() => this.initChart())
+  },
+  beforeDestroy () {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart () {
+      this.chart = echarts.init(document.getElementById('taskMonthCountChart'), 'macarons')
+      this.setOptions()
+    },
+    setOptions () {
+      this.chart.setOption({
+        backgroundColor: 'transparent',
+        textStyle: {
+          color: '#dddddd'
+        },
+        legend: {
+          data: ['发包数量', '接包数量'],
+          textStyle: {
+            color: '#dddddd'
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: this.yearMonths
+        },
+        yAxis: {
+          type: 'value'
+        },
+        series: [
+          {
+            name: '发包数量',
+            data: this.sendTaskCounts,
+            type: 'line',
+            smooth: true
+          },
+          {
+            name: '接包数量',
+            data: this.recvTaskCounts,
+            type: 'line',
+            smooth: true
+          },
+        ]
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.chart-title{
+  padding: 5px 8px 0px 8px;
+  font-weight: bold;
+  font-size: 18px;
+}
+</style>

+ 1 - 1
src/pages/Statistics/components/StaffDistributionMap/index.vue → src/pages/Statistics/components/map/StaffDistributionMap.vue

@@ -150,7 +150,7 @@ export default {
             data: myData.map(function (dataItem) {
               return {
                 name: dataItem[1].name,
-                value: geoCoordMap[dataItem[0].name].concat([dataItem[1].name]),
+                value: geoCoordMap[dataItem[0].name] ? geoCoordMap[dataItem[0].name].concat([dataItem[1].name]) : '',
                 tooltip: {
                   formatter: dataItem[0].name + '--' + dataItem[1].name + ':' + dataItem[0].value
                 }

+ 156 - 0
src/pages/Statistics/components/map/TotalMap.vue

@@ -0,0 +1,156 @@
+<template>
+  <div id="map-wrapper" style="width:100%; height: 100%;"/>
+</template>
+
+<script>
+export default {
+  name: 'TotalMap',
+  data: function () {
+    return {
+      provinceCountDatas: [],
+      provinces: [
+        '北京市',
+        '天津市',
+        '河北省',
+        '山西省',
+        '内蒙古自治区',
+        '辽宁省',
+        '吉林省',
+        '黑龙江省',
+        '上海市',
+        '江苏省',
+        '浙江省',
+        '安徽省',
+        '福建省',
+        '江西省',
+        '山东省',
+        '河南省',
+        '湖北省',
+        '湖南省',
+        '广东省',
+        '广西壮族自治区',
+        '海南省',
+        '重庆市',
+        '四川省',
+        '贵州省',
+        '云南省',
+        '西藏自治区',
+        '陕西省',
+        '甘肃省',
+        '青海省',
+        '宁夏回族自治区',
+        '新疆维吾尔自治区',
+        '台湾省',
+        '香港特别行政区',
+        '澳门特别行政区'
+      ]
+    }
+  },
+  props: {
+    provinceCounts: {
+      type: Array
+    }
+  },
+  watch: {
+    provinceCounts (newVal, oldVal) {
+      this.provinceCountDatas = []
+      let provinceCountMap = {}
+      newVal.forEach(provinceCountInfo => {
+        let countInfo = {}
+        countInfo['testerCount'] = provinceCountInfo.testerCount
+        countInfo['companyCount'] = provinceCountInfo.companyCount
+        countInfo['orgCount'] = provinceCountInfo.orgCount
+        countInfo['projectCount'] = provinceCountInfo.projectCount
+        countInfo['taskCount'] = provinceCountInfo.taskCount
+        provinceCountMap[provinceCountInfo.name] = countInfo
+      })
+      this.provinces.forEach(province => {
+        let countInfo = provinceCountMap[province]
+        if (!countInfo) {
+          countInfo = {'testerCount': 0, 'companyCount': 0, 'orgCount': 0, 'projectCount': 0, 'taskCount': 0}
+        }
+        this.provinceCountDatas.push({'name': this.changeProvinceName(province), 'value': countInfo})
+      })
+      this.initChart()
+    }
+  },
+  mounted () {
+    this.$nextTick(() => this.initChart())
+  },
+  methods: {
+    changeProvinceName (provinceName) {
+      if (provinceName.indexOf('内蒙古') != -1) {
+        return '内蒙古'
+      } else if (provinceName.indexOf('广西') != -1) {
+        return '广西'
+      } else if (provinceName.indexOf('西藏') != -1) {
+        return '西藏'
+      } else if (provinceName.indexOf('宁夏') != -1) {
+        return '宁夏'
+      } else if (provinceName.indexOf('新疆') != -1) {
+        return '新疆'
+      } else if (provinceName.indexOf('香港') != -1) {
+        return '香港'
+      } else if (provinceName.indexOf('澳门') != -1) {
+        return '澳门'
+      } else {
+        return provinceName.substring(0, provinceName.length - 1)
+      }
+    },
+    initChart () {
+      // 基于准备好的dom,初始化echarts实例
+      const chart = this.$echarts.init(document.getElementById('map-wrapper'))
+      // 地图展现数据
+      const series = [
+        {
+          type: 'map',
+          map: 'china',
+          label: {
+            show: true,
+            textStyle: {
+              fontSize: 12,
+              color: '#dddddd'
+            }
+          },
+          itemStyle: {
+            // normal: {
+            //   color: '#062031',
+            //   borderWidth: 1.1,
+            //   borderColor: '#43D0D6'
+            // },
+            emphasis: {
+              areaColor: '#43D0D6'
+            },
+            areaColor: '#117a8b'
+          },
+          data: this.provinceCountDatas
+        }
+      ]
+      const option = {
+        backgroundColor: 'transparent',
+        tooltip: {
+          trigger: 'item',
+          formatter: function (params, ticket, callback) {
+            return params.name + '<br/>众测工人:' + params.data.value.testerCount + '<br/>注册公司:' + params.data.value.companyCount +
+              '<br/>众测机构:' + params.data.value.orgCount + '<br/>项目发包:' + params.data.value.projectCount + '<br/>任务发包:' + params.data.value.taskCount
+          },
+          textStyle: {
+            fontSize: 10
+          }
+        },
+        series: series
+      }
+      chart.setOption(option)
+      window.onresize = () => {
+        // 这里使用箭头函数,避免this指向问题
+        chart && chart.resize()
+      }
+    }
+  }
+}
+
+</script>
+
+<style scoped>
+
+</style>

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


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

@@ -139,6 +139,9 @@ export default {
         screenshots: []
       }
     },
+    clearDefect () {
+      this.defect = this.init()
+    },
     submitForm (callback) {
       this.$refs['defectForm'].validate(valid => {
         if (valid) {

+ 13 - 2
src/pages/TestCase/components/defect_list.vue

@@ -6,6 +6,7 @@
       fit
       style="width: 100%"
       v-loading="listLoading"
+      max-height="500"
     >
       <el-table-column label="编号" align="center" min-width="15%">
         <template slot-scope="{row}">
@@ -107,7 +108,8 @@ export default {
       dialogFormVisible: false,
       textMap: {
         create: '新建缺陷',
-        update: '编辑缺陷'
+        update: '编辑缺陷',
+        copy: '复制缺陷'
       },
       defectData: {},
       listLoading: false,
@@ -151,6 +153,15 @@ export default {
       this.getList()
     }
   },
+  watch: {
+    dialogFormVisible: {
+      handler (nv, ov) {
+        if (!nv) {
+          this.$refs.defectForm.clearDefect()
+        }
+      }
+    }
+  },
   methods: {
     ...TestCaseUtils,
     ...mapGetters(['getRefreshTestCaseListFunc', 'getRefreshDefectListFunc']),
@@ -216,7 +227,7 @@ export default {
         this.defectData.expectedResult = testCase.expectedResult
         this.defectData.testResult = testCase.testResult
       }
-      this.dialogStatus = 'create_defect'
+      this.dialogStatus = 'create'
       this.dialogFormVisible = true
       this.$nextTick(() => {
         this.$refs['defectForm'].clearValidate()

+ 238 - 0
src/pages/TestCase/components/mix_defect_list.vue

@@ -0,0 +1,238 @@
+<template>
+  <div>
+    <el-table
+      :data="mixDefects"
+      border
+      fit
+      style="width: 100%"
+      v-loading="listLoading"
+      max-height="500"
+    >
+      <el-table-column type="expand" align="center" min-width="1%">
+        <template slot-scope="{row, $index}">
+          <mix-supp-defect-list :defects="row.suppDefects" :master-index="$index" ref="innerDefectList" :master-defect="row.masterDefect" :change-master="changeMaster" :add-master="addMaster" :add-supp="addSupp" :get-masters="getMasters"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="编号" align="center" min-width="15%">
+        <template slot-scope="{row}">
+          <span>{{ row.masterDefect.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.masterDefect.descr }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="严重等级" align="center" min-width="12%">
+        <template slot-scope="{row}">
+          <span>{{ toSeriousnessCn(row.masterDefect.seriousness) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="优先级" align="center" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ toPriorityCn(row.masterDefect.priority) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="缺陷类型" align="center" min-width="12%">
+        <template slot-scope="{row}">
+          <span>{{ toDefectTypeCn(row.masterDefect.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.masterDefect.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.masterDefect.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.masterDefect.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.masterDefect.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.masterDefect)" style="cursor: pointer;" title="查看详情"></i>
+          <i class="el-icon-refresh-left mini-margin" v-if="!(row.suppDefects.length)" @click="masterDialogFormVisible = true; movedDefectId = row.masterDefect.id; movedDefectIndex = $index" style="cursor: pointer;" title="转移到另外一个主记录底下"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <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>
+
+    <el-dialog title="选择主记录" :visible.sync="masterDialogFormVisible" :close-on-click-modal="false">
+      <el-form label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="主记录">
+          <el-select v-model="selectedMasterId" filterable :filter-method="dataFilter" @click.native="eqNoClick" :clearable="true">
+            <el-option
+              v-for="master in searchMasters"
+              :key="master.id"
+              :label="master.descr"
+              :value="master.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="masterDialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="handleToOtherMasterDefect()">
+          确定
+        </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'
+import DefectDetail from './defect_detail'
+import MixSuppDefectList from './mix_supp_defect_list'
+import DefectList from './defect_list'
+import TestCaseUtils from '../utils'
+
+export default {
+  name: 'MixDefectList',
+  components: { DefectDetail, DefectList, MixSuppDefectList },
+  data: function () {
+    return {
+      detailDialogFormVisible: false,
+      masterDialogFormVisible: false,
+      mixDefects: [],
+      listLoading: false,
+      defectData: {},
+      searchMasters: [],
+      selectedMasterId: undefined,
+      movedDefectId: undefined,
+      movedDefectIndex: undefined
+    }
+  },
+  props: {
+    selectedTaskCode: {
+      type: String,
+      default: ''
+    },
+    setMixDefectsEmpty: {
+      type: Function
+    }
+  },
+  watch: {
+    selectedTaskCode: {
+      immediate: true,
+      handler (nv, ov) {
+        console.log(2, nv)
+        if (nv) {
+          this.getList()
+        }
+      }
+    }
+  },
+  methods: {
+    ...TestCaseUtils,
+    changeMaster (defect, index) {
+      deepClone(this.mixDefects[index].masterDefect, defect)
+    },
+    addMaster (defect, index) {
+      this.mixDefects.push({'masterDefect': defect, 'suppDefects': []})
+    },
+    addSupp (masterId, defect) {
+      let mixDefect = this.mixDefects.find(mixDefect => mixDefect.masterDefect.id === masterId)
+      mixDefect.suppDefects.push(defect)
+    },
+    getMasters () {
+      let arr = []
+      this.mixDefects.forEach(mixDefect => arr.push(mixDefect.masterDefect))
+      return arr
+    },
+    handleDetail (row) {
+      deepClone(this.defectData, row)
+      this.detailDialogFormVisible = true
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    getList () {
+      this.showListLoading()
+      let url = Api.MIXDEFECT.LIST.replace('{taskCode}', this.selectedTaskCode)
+      Http.get(url).then((res) => {
+        this.mixDefects = res.data
+        this.setMixDefectsEmpty(!(this.mixDefects.length))
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取缺陷数据失败:' + error.data.message)
+      })
+      this.hideListLoading()
+    },
+    handleToOtherMasterDefect () {
+      this.$confirm('您确定要迁移到其他主记录底下吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        Http.post(Api.MIXDEFECT.TOOTHERMASTERDEFECT.replace('{masterDefectId}', this.selectedMasterId).replace('{movedDefectId}', this.movedDefectId), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '迁移成功')
+            let defect = this.mixDefects[this.movedDefectIndex].masterDefect
+            this.mixDefects.splice(this.movedDefectIndex, 1)
+            this.addSupp(this.selectedMasterId, defect)
+            this.masterDialogFormVisible = false
+          } else {
+            notify('error', '迁移失败:' + res.data)
+          }
+        }).catch((error) => {
+          notify('error', '迁移失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    dataFilter (val) {
+      if (val) {
+        this.searchMasters = this.mixDefects.filter(mixDefect => {
+          return mixDefect.masterDefect.id !== this.movedDefectId && mixDefect.masterDefect.descr.includes(val)
+        }).map(mixDefect => mixDefect.masterDefect)
+      } else {
+        this.searchMasters = this.mixDefects.filter(mixDefect => {
+          return mixDefect.masterDefect.id !== this.movedDefectId
+        }).map(mixDefect => mixDefect.masterDefect)
+      }
+    },
+    eqNoClick () {
+      this.searchMasters = this.mixDefects.filter(mixDefect => {
+        return mixDefect.masterDefect.id !== this.movedDefectId
+      }).map(mixDefect => mixDefect.masterDefect)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mini-margin {
+  margin-left: 6px;
+  margin-right: 6px;
+}
+</style>

+ 261 - 0
src/pages/TestCase/components/mix_supp_defect_list.vue

@@ -0,0 +1,261 @@
+<template>
+  <div>
+    <el-table
+      :data="defectDatas"
+      border
+      fit
+      style="width: 100%"
+      max-height="500"
+    >
+      <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)" style="cursor: pointer;" title="查看详情"></i>
+          <i class="el-icon-sort mini-margin" @click="handleExchange(row)" style="cursor: pointer;" title="与主记录互换位置"></i>
+          <i class="el-icon-top mini-margin" @click="handleToMaster(row, $index)" style="cursor: pointer;" title="独立为主记录"></i>
+          <i class="el-icon-refresh-left mini-margin" @click="masterDialogFormVisible = true; movedDefectId = row.id; movedDefectIndex = $index" style="cursor: pointer;" title="转移到另外一个主记录底下"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <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>
+
+    <el-dialog title="选择主记录" :visible.sync="masterDialogFormVisible" :close-on-click-modal="false">
+      <el-form label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="主记录">
+          <el-select v-model="selectedMasterId" filterable :filter-method="dataFilter" @click.native="eqNoClick" :clearable="true">
+            <el-option
+              v-for="master in searchMasters"
+              :key="master.id"
+              :label="master.descr"
+              :value="master.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="masterDialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="handleToOtherMasterDefect()">
+          确定
+        </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'
+import DefectDetail from './defect_detail'
+import DefectList from './defect_list'
+import TestCaseUtils from '../utils'
+
+export default {
+  name: 'MixDefectList',
+  components: { DefectDetail, DefectList },
+  data: function () {
+    return {
+      detailDialogFormVisible: false,
+      masterDialogFormVisible: false,
+      defectDatas: [],
+      defectData: {},
+      masters: [],
+      searchMasters: [],
+      selectedMasterId: undefined,
+      movedDefectId: undefined,
+      movedDefectIndex: undefined
+    }
+  },
+  props: {
+    defects: {
+      type: Array
+    },
+    masterDefect: {
+      type: Object
+    },
+    changeMaster: {
+      type: Function
+    },
+    addMaster: {
+      type: Function
+    },
+    addSupp: {
+      type: Function
+    },
+    getMasters: {
+      type: Function
+    },
+    masterIndex: {
+      type: Number
+    }
+  },
+  watch: {
+    defects: {
+      immediate: true,
+      handler (nv, ov) {
+        this.defectDatas = nv
+      }
+    },
+    masterDialogFormVisible: {
+      handler (nv, ov) {
+        if (!nv) {
+          this.masters = []
+        } else {
+          this.masters = this.getMasters()
+        }
+      }
+    }
+  },
+  methods: {
+    ...TestCaseUtils,
+    handleDetail (row) {
+      deepClone(this.defectData, row)
+      this.detailDialogFormVisible = true
+    },
+    handleExchange (row) {
+      this.$confirm('您确定要与主记录交换位置吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        Http.post(Api.MIXDEFECT.EXCHANGE.replace('{masterDefectId}', this.masterDefect.id).replace('{suppDefectId}', row.id), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '交换成功')
+            let copyRow = {}
+            deepClone(copyRow, row)
+            deepClone(row, this.masterDefect)
+            this.changeMaster(copyRow, this.masterIndex)
+          } else {
+            notify('error', '交换失败:' + res.data)
+          }
+        }).catch((error) => {
+          notify('error', '交换失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    handleToMaster (row, index) {
+      this.$confirm('您确定要独立为主记录吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        Http.post(Api.MIXDEFECT.TOMASTER.replace('{suppDefectId}', row.id), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '独立成功')
+            this.defectDatas.splice(index, 1)
+            this.addMaster(row)
+          } else {
+            notify('error', '独立失败:' + res.data)
+          }
+        }).catch((error) => {
+          notify('error', '独立失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    handleToOtherMasterDefect () {
+      this.$confirm('您确定要迁移到其他主记录底下吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        Http.post(Api.MIXDEFECT.TOOTHERMASTERDEFECT.replace('{masterDefectId}', this.selectedMasterId).replace('{movedDefectId}', this.movedDefectId), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '迁移成功')
+            let defect = this.defectDatas[this.movedDefectIndex]
+            this.defectDatas.splice(this.movedDefectIndex, 1)
+            this.addSupp(this.selectedMasterId, defect)
+            this.masterDialogFormVisible = false
+          } else {
+            notify('error', '迁移失败:' + res.data)
+          }
+        }).catch((error) => {
+          notify('error', '迁移失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    dataFilter (val) {
+      if (val) {
+        this.searchMasters = this.masters.filter(master => {
+          return master.descr.includes(val)
+        })
+      } else {
+        this.searchMasters = this.masters
+      }
+    },
+    eqNoClick () {
+      this.searchMasters = this.masters
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mini-margin {
+  margin-left: 6px;
+  margin-right: 6px;
+}
+</style>

+ 40 - 6
src/pages/TestCase/components/test_case_list.vue

@@ -7,7 +7,8 @@
       border
       fit
       highlight-current-row
-      style="width: 100%"
+      style="width: 100%; "
+      max-height="500"
     >
       <el-table-column type="expand" align="center" min-width="1%">
         <template slot-scope="{row, $index}">
@@ -107,7 +108,7 @@
     <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 @click="detailDialogFormVisible = false">
           取消
         </el-button>
         <el-button type="primary" @click="submitAudit()" v-if="canAudit">
@@ -152,7 +153,8 @@ export default {
       dialogStatus: '',
       textMap: {
         update: '编辑测试用例',
-        create: '新建测试用例'
+        create: '新建测试用例',
+        copy: '复制'
       }
     }
   },
@@ -186,6 +188,15 @@ export default {
     this.initData()
     this.getList()
   },
+  watch: {
+    dialogFormVisible: {
+      handler (nv, ov) {
+        if (!nv) {
+          this.$refs.testCaseForm.clearTestCaseData()
+        }
+      }
+    }
+  },
   methods: {
     ...TestCaseUtils,
     ...mapGetters(['getRefreshTestCaseListFunc', 'getRefreshDefectListFunc']),
@@ -266,11 +277,32 @@ export default {
     submit () {
       let callback = null
       if (this.testCaseData.id) {
-        callback = this.getList
+        callback = () => {
+          this.getList()
+          this.dialogFormVisible = false
+        }
       } else {
         callback = () => {
           this.listQueryParam.pageNo = Math.ceil((this.total + 1) / this.listQueryParam.pageSize)
           this.getList()
+          if (this.dialogStatus === 'create' || this.dialogStatus === 'copy') {
+            let title = this.dialogStatus === 'create' ? '是否继续新增下一条?' : '是否继续以当前提交的数据为模板复制下一条?'
+            this.$confirm(title, '提示', {
+              confirmButtonText: '确定',
+              cancelButtonText: '取消',
+              type: 'warning',
+              center: true,
+              closeOnClickModal: false
+            }).then(() => {
+              if (this.dialogStatus === 'create') {
+                this.$refs.testCaseForm.clearTestCaseData()
+              } else if (this.dialogStatus === 'copy') {
+                this.$refs.testCaseForm.clearId()
+              }
+            }).catch(() => {
+              this.dialogFormVisible = false
+            })
+          }
         }
       }
       this.$refs.testCaseForm.submitForm(callback)
@@ -288,7 +320,7 @@ export default {
       })
     },
     handleDelete (row, index) {
-      this.$confirm('您确定要删除该条数据吗?', '提示', {
+      this.$confirm('删除该用例会连同该用例底下的缺陷记录一起删除,您确定要删除该用例吗?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
@@ -301,6 +333,7 @@ export default {
           if (res.code === 20000) {
             notify('success', '删除成功')
             this.getList()
+            this.getRefreshDefectListFunc()()
           } else {
             notify('error', '删除失败:' + res.data)
           }
@@ -312,7 +345,7 @@ export default {
       })
     },
     handleCopy (row) {
-      this.dialogStatus = 'create'
+      this.dialogStatus = 'copy'
       this.dialogFormVisible = true
       deepClone(this.testCaseData, row)
       this.testCaseData.id = undefined
@@ -326,6 +359,7 @@ export default {
         let testCase = this.testCaseDatas.find(testCase => testCase.id === this.testCaseData.id)
         testCase.examStatus = examStatus
         testCase.examDescr = examDescr
+        this.detailDialogFormVisible = false
       })
     },
     showListLoading () {

+ 2 - 2
src/pages/TestCase/components/test_tool_list.vue

@@ -82,8 +82,8 @@ export default {
     return {
       dialogFormVisible: false,
       textMap: {
-        create: '新建测试环境',
-        update: '编辑测试环境'
+        create: '新建测试工具',
+        update: '编辑测试工具'
       },
       testToolData: {},
       listLoading: false,

+ 10 - 4
src/pages/TestCase/components/testcase_form.vue

@@ -167,15 +167,21 @@ export default {
         associatedCode: ''
       }
     },
+    clearTestCaseData () {
+      deepClone(this.testCase, this.testCaseData)
+    },
+    clearId () {
+      this.testCase.id = undefined
+    },
     getList () {
       let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.testCase.taskCode)
-        .replace('{designerId}', this.testCase.designerId)
+        .replace('{designerId}', this.testCase.designerId) // TODO
         .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
+          return testCase.id !== this.testCase.id
         })
         this.searchTestCases = this.testCases
       }).catch((error) => {
@@ -226,7 +232,7 @@ export default {
             } else {
               notify('success', '提交成功')
               if (!this.testCase.id) {
-                deepClone(this.testCase, this.testCaseData)
+                // deepClone(this.testCase, this.testCaseData)
                 this.clearValidate()
               }
               callback()
@@ -234,11 +240,11 @@ export default {
           }).catch((error) => {
             this.hideLoading()
             notify('error', '用例创建失败:' + error)
+            return false
           })
           // 提交 report
         } else {
           notify('error', '表单填写有误')
-          return false
         }
       })
     },

+ 27 - 2
src/pages/TestCase/exam_testcases.vue

@@ -38,6 +38,9 @@
         <el-option value="VALID" label="有效"/>
         <el-option value="INVALID" label="无效"/>
       </el-select>
+      <el-button v-if="canAudit" class="filter-item" style="margin-left: 10px;" type="success" @click="handleAuditAllValid">
+        全部审核通过
+      </el-button>
     </div>
     <el-tabs v-model="tabActiveName" @tab-click="handleTabClick">
       <el-tab-pane label="测试用例" name="testCases">
@@ -116,8 +119,9 @@ export default {
     },
     'selectedUserId': {
       immediate: false,
-      handler: function () {
-        if (this.selectedUserId) {
+      handler: function (nv, ov) {
+        if (nv) {
+          this.selectedUser = this.users.find(user => user.id === this.selectedUserId)
           this.getList(false)
         }
       }
@@ -262,6 +266,27 @@ export default {
         this.testStatusSearchShow = false
         this.examStatusSearchShow = false
       }
+    },
+    handleAuditAllValid () {
+      this.$confirm('您确定要把所有测试用例的审核状态设置为有效吗?一旦设置成功,之前设置为无效的测试用例的状态和审核结果说明都将被覆盖!', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        Http.post(Api.TESTCASE.EXAM_ALL_VALID.replace('{taskCode}', this.selectedTaskCode).replace('{designerId}', this.selectedUserId), {}).then((res) => {
+          if (res.code === 20000) {
+            notify('success', '审核成功')
+            this.$refs.testCaseList.getList()
+          } else {
+            notify('error', '审核失败:' + res.data)
+          }
+        }).catch((error) => {
+          notify('error', '审核失败:系统异常')
+        })
+      }).catch(() => {
+      })
     }
   }
 }

+ 175 - 0
src/pages/TestCase/mix_defects.vue

@@ -0,0 +1,175 @@
+<template>
+  <div class="app-container">
+    <div class="title h1" style="margin-top: 10px;">缺陷报告融合</div>
+    <div class="filter-container">
+      <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>
+      <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>
+      <el-button class="filter-item" v-if="bgrhStatus < 1" style="margin-left: 10px;" type="primary" @click="">
+        生成融合数据
+      </el-button>
+      <el-button :disabled="true" class="filter-item" v-if="bgrhStatus === 1" style="margin-left: 10px;" type="primary" @click="exportReport()">
+        融合数据生成中...
+      </el-button>
+      <el-button class="filter-item" v-if="bgrhStatus === 2" style="margin-left: 10px;" type="primary" @click="exportReport()">
+        导出当前任务融合报告
+      </el-button>
+      <el-button class="filter-item" v-if="bgrhStatus === 2" style="margin-left: 10px;" type="primary" @click="exportProjectReport()">
+        导出整个项目融合报告
+      </el-button>
+    </div>
+    <div>
+      <mix-defect-list mix-defects="mixDefects" ref="mixDefectList" :selected-task-code="selectedTaskCode" :set-mix-defects-empty="setMixDefectsEmpty"></mix-defect-list>
+    </div>
+  </div>
+</template>
+
+<script>
+import MixDefectList from './components/mix_defect_list'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+
+export default {
+  name: 'MixDefects',
+  components: { MixDefectList },
+  data () {
+    return {
+      selectedProjectCode: this.$route.params.projectCode,
+      selectedTaskCode: this.$route.params.taskCode,
+      selectedTaskData: {
+        name: '',
+        code: '',
+        status: undefined,
+        id: undefined
+      },
+      projects: [],
+      searchProjects: [],
+      tasks: [],
+      searchTasks: [],
+      mixDefectsEmpty: true,
+      bgrhStatus: true
+    }
+  },
+  watch: {
+    'selectedProjectCode': {
+      immediate: false,
+      handler: function () {
+        this.getSimpleTaskDatas(false)
+      }
+    },
+    'selectedTaskCode': {
+      immediate: false,
+      handler: function () {
+        this.getBgrhStatus()
+      }
+    }
+  },
+  created () {
+    this.getSimpleProjectDatas()
+    this.getSimpleTaskDatas(true)
+    this.getBgrhStatus()
+  },
+  methods: {
+    getBgrhStatus () {
+      Http.get(Api.MIXDEFECT.STATUS.replace('{taskCode}', this.selectedTaskCode)).then((res) => {
+        this.bgrhStatus = res.data
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '获取报告融合状态异常:系统异常')
+      })
+    },
+    generateBgrh () {
+      Http.post(Api.MIXDEFECT.GENERATE.replace('{taskCode}', this.selectedTaskCode)).then((res) => {
+        if (res.code === 20000) {
+          notify('success', '报告融合数据生成中...')
+          this.bgrhStatus = 1
+        } else {
+          notify('error', '生成报告融合数据失败')
+        }
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '生成报告融合数据异常:系统异常')
+      })
+    },
+    setMixDefectsEmpty (state) {
+      this.mixDefectsEmpty = state
+    },
+    getList () {
+      this.$refs.mixDefectList.getList()
+    },
+    projectDataFilter (val) {
+      if (val) {
+        this.searchProjects = this.projects.filter(project => {
+          return project.name.includes(val)
+        })
+      } else {
+        this.searchProjects = this.projects
+      }
+    },
+    taskDataFilter (val) {
+      if (val) {
+        this.searchTasks = this.tasks.filter(task => {
+          return task.name.includes(val)
+        })
+      } else {
+        this.searchTasks = this.tasks
+      }
+    },
+    eqNoClick () {
+      this.searchProjects = this.projects
+      this.searchTasks = this.tasks
+    },
+    getSimpleProjectDatas () {
+      Http.get(Api.PROJECT.GET_SIMPLE_DATAS).then((res) => {
+        this.projects = res.data
+        this.searchProjects = res.data
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '获取项目列表数据失败:系统异常')
+      })
+    },
+    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 (!firstIn) {
+          this.selectedTaskData = this.tasks[0]
+          this.selectedTaskCode = this.selectedTaskData.code
+        } else {
+          this.selectedTaskData = this.searchTasks.find(task => task.code === this.selectedTaskCode)
+        }
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '获取任务列表数据失败:系统异常')
+      })
+    },
+    exportReport () {
+      window.location.href = Api.MIXDEFECT.EXPORT.replace('{taskCode}', this.selectedTaskCode)
+    },
+    exportProjectReport () {
+      window.location.href = Api.MIXDEFECT.EXPORT_PROJECT.replace('{projectCode}', this.selectedProjectCode)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.filter-container {
+  margin-bottom: 4px;
+  padding-left: 6px;
+}
+</style>

+ 18 - 18
src/pages/TestCase/testcases.vue

@@ -22,22 +22,22 @@
         <el-option value="VALID" label="有效"/>
         <el-option value="INVALID" label="无效"/>
       </el-select>
-<!--      <el-upload style="display: inline-block;"-->
-<!--        :action="uploadTestCasesFileUrl"-->
-<!--        :on-success="handleUploadTestCasesFileSuccess"-->
-<!--        :before-upload="beforeTestCasesFileUpload"-->
-<!--        :on-error="handleUploadTestCasesFileError"-->
-<!--        :show-file-list="false" v-if="testCaseCanEdit">-->
-<!--        <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" v-if="testCaseCanEdit">-->
-<!--        <el-button class="filter-item" style="margin-left: 10px;" type="primary" >上传缺陷报告</el-button>-->
-<!--      </el-upload>-->
+      <el-upload style="display: inline-block;"
+        :action="uploadTestCasesFileUrl"
+        :on-success="handleUploadTestCasesFileSuccess"
+        :before-upload="beforeTestCasesFileUpload"
+        :on-error="handleUploadTestCasesFileError"
+        :show-file-list="false" v-if="testCaseCanEdit">
+        <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" v-if="testCaseCanEdit">
+        <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>
@@ -50,7 +50,7 @@
       <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>-->
+      <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>
 
@@ -167,7 +167,7 @@ export default {
         center: true,
         closeOnClickModal: false
       }).then(() => {
-        Http.put(Api.TASK.COMMIT_TASK.replace('{taskCode}', this.selectedTaskCode), {}).then((res) => {
+        Http.put(Api.TASK.COMMIT_USER_TASK.replace('{taskCode}', this.selectedTaskCode), {}).then((res) => {
           if (res.code === 20000) {
             notify('success', '提交成功')
             this.testCaseCanEdit = false

+ 31 - 0
src/pages/UserCenter/Cert.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="right-qualification">
+    <div class="right-qualification-title">
+      <span style="font-size: 18px;font-weight: bold">我的证书</span>
+    </div>
+    <div style="margin-bottom: 15px">
+      <tester-cert/>
+    </div>
+  </div>
+</template>
+
+<script>
+  import TesterCert from './components/TesterCert'
+  export default {
+    name: "Cert",
+    components: {TesterCert}
+  }
+</script>
+<style scoped lang="scss">
+.right-qualification {
+  padding: 20px;
+  background: rgba(255, 255, 255, 1);
+  box-shadow: 0px 1px 6px 0px rgba(8, 6, 6, 0.13);
+
+  .right-qualification-title {
+    padding: 10px;
+    border-bottom: 1px solid #ccc;
+    margin-bottom: 20px;
+  }
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 173 - 0
src/pages/UserCenter/components/TesterCert.vue


+ 19 - 0
src/router/index.js

@@ -485,6 +485,10 @@ export default new Router({
         {
           path: '',
           redirect: '/personal/mine'
+        },
+        {
+          path: '/personal/cert',
+          component: resolve => require(['@/pages/UserCenter/Cert.vue'], resolve)
         }
       ]
     },
@@ -515,6 +519,21 @@ export default new Router({
       name: 'TesterTaskPage',
       path: '/tester/:userId/:taskCode',
       component: resolve => require(['@/pages/Tester/testertask.vue'], resolve)
+    },
+    {
+      name: 'StatisticsPage',
+      path: '/allstatistics',
+      component: resolve => require(['@/pages/Statistics/Statistics.vue'], resolve)
+    },
+    {
+      name: 'MixDefectPage',
+      path: '/mixdefects/:projectCode/:taskCode',
+      component: resolve => require(['@/pages/TestCase/mix_defects.vue'], resolve)
+    },
+    {
+      name: 'TaskAmountCalPage',
+      path: '/taskamount/:taskCode',
+      component: resolve => require(['@/pages/Amount/TaskAmountCal.vue'], resolve)
     }
   ]
 })

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels