From be6390d3bf8f6d350b5a7ea663c118bf0a20a0d1 Mon Sep 17 00:00:00 2001 From: song-tianyang Date: Sun, 26 Sep 2021 16:31:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(=20=E6=8A=A5=E8=A1=A8=E7=BB=9F=E8=AE=A1):?= =?UTF-8?q?=20=E6=8A=A5=E8=A1=A8=E7=BB=9F=E8=AE=A1=E4=BB=8Expack=E6=8C=AA?= =?UTF-8?q?=E5=88=B0=E5=BC=80=E6=BA=90=E7=89=88=EF=BC=8C=E5=B9=B6=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=BF=9D=E5=AD=98=E3=80=81=E5=AF=BC=E5=87=BA=E7=9A=84?= =?UTF-8?q?=E6=9D=83=E9=99=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 报表统计从xpack挪到开源版,并增加保存、导出的权限。 --- .../mapper/ext/ExtTestAnalysisMapper.java | 14 + .../base/mapper/ext/ExtTestAnalysisMapper.xml | 100 +++ .../mapper/ext/ExtTestCaseCountMapper.java | 65 ++ .../mapper/ext/ExtTestCaseCountMapper.xml | 219 +++++ .../controller/HistoryReportController.java | 56 ++ .../controller/TestAnalysisController.java | 24 + .../controller/TestCaseCountController.java | 34 + .../reportstatistics/dto/PieChartDTO.java | 27 + .../dto/ReportStatisticsSaveRequest.java | 14 + .../dto/ReportStatisticsType.java | 5 + .../dto/TestAnalysisChartDTO.java | 29 + .../dto/TestAnalysisChartRequest.java | 21 + .../dto/TestAnalysisChartResult.java | 12 + .../dto/TestAnalysisResult.java | 22 + .../dto/TestAnalysisTableDTO.java | 29 + .../dto/TestCaseCountChartResult.java | 15 + .../dto/TestCaseCountRequest.java | 79 ++ .../dto/TestCaseCountResponse.java | 24 + .../dto/TestCaseCountSummary.java | 22 + .../dto/TestCaseCountTableDTO.java | 34 + .../reportstatistics/dto/charts/Legend.java | 20 + .../reportstatistics/dto/charts/PieData.java | 20 + .../reportstatistics/dto/charts/Series.java | 20 + .../reportstatistics/dto/charts/Title.java | 14 + .../reportstatistics/dto/charts/XAxis.java | 17 + .../reportstatistics/dto/charts/YAxis.java | 14 + .../service/ReportStatisticsService.java | 77 ++ .../service/TestAnalysisService.java | 171 ++++ .../service/TestCaseCountService.java | 775 ++++++++++++++++++ backend/src/main/java/io/metersphere/xpack | 2 +- backend/src/main/resources/permission.json | 12 + .../components/common/router/router.js | 10 +- .../common/select-tree/SelectTree.vue | 26 +- .../reportstatistics/ReportAnalysis.vue | 101 +++ .../reportstatistics/ReportCard.vue | 138 ++++ .../base/HistoryReportData.vue | 86 ++ .../reportstatistics/base/ReportHeader.vue | 115 +++ .../base/compose/HistoryReportDataCard.vue | 86 ++ .../components/reportstatistics/router.js | 16 + .../testCaseCount/TestCaseCountContainer.vue | 217 +++++ .../chart/TestCaseCountChart.vue | 284 +++++++ .../filter/TestCaseCountFilter.vue | 401 +++++++++ .../table/TestCaseCountTable.vue | 113 +++ .../track/TestAnalysisContainer.vue | 168 ++++ .../track/chart/TestAnalysisChart.vue | 143 ++++ .../track/filter/TestAnalysisFilter.vue | 225 +++++ .../track/table/TestAnalysisTable.vue | 77 ++ frontend/src/business/components/xpack | 2 +- frontend/src/i18n/en-US.js | 1 + frontend/src/i18n/zh-CN.js | 1 + frontend/src/i18n/zh-TW.js | 1 + 51 files changed, 4189 insertions(+), 9 deletions(-) create mode 100644 backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java create mode 100644 backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml create mode 100644 backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java create mode 100644 backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java create mode 100644 backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java create mode 100644 frontend/src/business/components/reportstatistics/ReportAnalysis.vue create mode 100644 frontend/src/business/components/reportstatistics/ReportCard.vue create mode 100644 frontend/src/business/components/reportstatistics/base/HistoryReportData.vue create mode 100644 frontend/src/business/components/reportstatistics/base/ReportHeader.vue create mode 100644 frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue create mode 100644 frontend/src/business/components/reportstatistics/router.js create mode 100644 frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue create mode 100644 frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue create mode 100644 frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue create mode 100644 frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue create mode 100644 frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue create mode 100644 frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue create mode 100644 frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue create mode 100644 frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java new file mode 100644 index 0000000000..87e088bccd --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java @@ -0,0 +1,14 @@ +package io.metersphere.base.mapper.ext; + + +import io.metersphere.reportstatistics.dto.TestAnalysisChartRequest; +import io.metersphere.reportstatistics.dto.TestAnalysisChartResult; + +import java.util.List; + +public interface ExtTestAnalysisMapper { + + List getCraeteCaseReport(TestAnalysisChartRequest request); + + List getUpdateCaseReport(TestAnalysisChartRequest request); +} diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml new file mode 100644 index 0000000000..c4c9e36489 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml @@ -0,0 +1,100 @@ + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java new file mode 100644 index 0000000000..060f2735ee --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java @@ -0,0 +1,65 @@ +package io.metersphere.base.mapper.ext; + +import io.metersphere.reportstatistics.dto.TestCaseCountChartResult; +import io.metersphere.reportstatistics.dto.TestCaseCountRequest; + +import java.util.List; + +public interface ExtTestCaseCountMapper { + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user + * maintainer + * '功能用例' + * status + * priority + * + * @ request + * @return + */ + List getFunctionCaseCount(TestCaseCountRequest request); + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user_id + * ----不知道 + * '接口用例' + * status + * ----不知道 + * + * @param request + * @return + */ + List getApiCaseCount(TestCaseCountRequest request); + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user + * principal + * '场景用例' + * status + * level + * + * @param request + * @return + */ + List getScenarioCaseCount(TestCaseCountRequest request); + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user + * follow_people + * '性能用例' + * status + * 不知道 + * + * @param request + * @return + */ + List getLoadCaseCount(TestCaseCountRequest request); +} diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml new file mode 100644 index 0000000000..33ea3c3bf3 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java b/backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java new file mode 100644 index 0000000000..ce62705bdb --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java @@ -0,0 +1,56 @@ +package io.metersphere.reportstatistics.controller; + +import com.alibaba.fastjson.JSONArray; +import io.metersphere.base.domain.ReportStatistics; +import io.metersphere.base.domain.ReportStatisticsWithBLOBs; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.reportstatistics.dto.ReportStatisticsSaveRequest; +import io.metersphere.reportstatistics.service.ReportStatisticsService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author song.tianyang + * @Date 2021/9/14 2:58 下午 + */ +@RestController +@RequestMapping(value = "/history/report") +public class HistoryReportController { + + @Resource + private ReportStatisticsService reportStatisticsService; + + @PostMapping("/selectByParams") + public List selectByParams(@RequestBody ReportStatisticsSaveRequest request) { + List returnList = reportStatisticsService.selectByProjectIdAndReportType(request.getProjectId(),request.getReportType()); + LogUtil.info("报表查询结果:"+JSONArray.toJSONString(returnList)); + return returnList; + } + + @PostMapping("/saveReport") + public ReportStatisticsWithBLOBs saveReport(@RequestBody ReportStatisticsSaveRequest request){ + ReportStatisticsWithBLOBs returnData = reportStatisticsService.saveByRequest(request); + return returnData; + } + + @PostMapping("/updateReport") + public ReportStatisticsWithBLOBs updateReport(@RequestBody ReportStatisticsSaveRequest request){ + ReportStatisticsWithBLOBs returnData = reportStatisticsService.updateByRequest(request); + return returnData; + } + + @PostMapping("/deleteByParam") + public int deleteById(@RequestBody ReportStatisticsSaveRequest request) { + return reportStatisticsService.deleteById(request.getId()); + } + + @PostMapping("/selectById") + public ReportStatisticsWithBLOBs selectById(@RequestBody ReportStatisticsSaveRequest request) { + return reportStatisticsService.selectById(request.getId()); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java new file mode 100644 index 0000000000..f7746b183b --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java @@ -0,0 +1,24 @@ +package io.metersphere.reportstatistics.controller; + +import io.metersphere.reportstatistics.dto.TestAnalysisChartRequest; +import io.metersphere.reportstatistics.dto.TestAnalysisResult; +import io.metersphere.reportstatistics.service.TestAnalysisService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping(value = "/report/test/analysis") +public class TestAnalysisController { + + @Resource + TestAnalysisService testAnalysisService; + + @PostMapping("/getReport") + public TestAnalysisResult getReport(@RequestBody TestAnalysisChartRequest request) { + return testAnalysisService.getReport(request); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java new file mode 100644 index 0000000000..d42d0d6b15 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java @@ -0,0 +1,34 @@ +package io.metersphere.reportstatistics.controller; + +import io.metersphere.reportstatistics.dto.TestCaseCountRequest; +import io.metersphere.reportstatistics.dto.TestCaseCountResponse; +import io.metersphere.reportstatistics.service.TestCaseCountService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(value = "/report/test/case/count") +public class TestCaseCountController { + + @Resource + TestCaseCountService testCaseCountService; + + @PostMapping("/initDatas") + public Map>> initDatas(@RequestBody TestCaseCountRequest request) { + Map>> returnMap = testCaseCountService.getSelectFilterDatas(request.getProjectId()); + + return returnMap; + } + + @PostMapping("/getReport") + public TestCaseCountResponse getReport(@RequestBody TestCaseCountRequest request) { + TestCaseCountResponse response = testCaseCountService.getReport(request); + return response; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java new file mode 100644 index 0000000000..c5ed790f2c --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java @@ -0,0 +1,27 @@ +package io.metersphere.reportstatistics.dto; + +import com.alibaba.fastjson.JSONObject; +import io.metersphere.reportstatistics.dto.charts.Series; +import io.metersphere.reportstatistics.dto.charts.Title; +import io.metersphere.reportstatistics.dto.charts.XAxis; +import io.metersphere.reportstatistics.dto.charts.YAxis; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class PieChartDTO { + private JSONObject dataset; + private JSONObject tooltip; + private XAxis xAxis; + private YAxis yAxis; + private List series; + private List title; + private int width; + + public PieChartDTO() { + tooltip = new JSONObject(); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java new file mode 100644 index 0000000000..a206aa2fdb --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java @@ -0,0 +1,14 @@ +package io.metersphere.reportstatistics.dto; + +import io.metersphere.base.domain.ReportStatisticsWithBLOBs; +import lombok.Getter; +import lombok.Setter; + +/** + * @author song.tianyang + * @Date 2021/9/14 4:51 下午 + */ +@Getter +@Setter +public class ReportStatisticsSaveRequest extends ReportStatisticsWithBLOBs { +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java new file mode 100644 index 0000000000..34dfbd8c9e --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java @@ -0,0 +1,5 @@ +package io.metersphere.reportstatistics.dto; + +public enum ReportStatisticsType { + TEST_CASE_COUNT,TEST_CASE_ANALYSIS +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java new file mode 100644 index 0000000000..8e10195daa --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java @@ -0,0 +1,29 @@ +package io.metersphere.reportstatistics.dto; + +import io.metersphere.reportstatistics.dto.charts.Legend; +import io.metersphere.reportstatistics.dto.charts.Series; +import io.metersphere.reportstatistics.dto.charts.XAxis; +import io.metersphere.reportstatistics.dto.charts.YAxis; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestAnalysisChartDTO { + private Legend legend; + private XAxis xAxis; + private YAxis yAxis; + private List<Series> series; + + public TestAnalysisChartDTO() { + } + + public TestAnalysisChartDTO(Legend legend, XAxis xAxis, YAxis yAxis, List<Series> series) { + this.legend = legend; + this.xAxis = xAxis; + this.yAxis = yAxis; + this.series = series; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java new file mode 100644 index 0000000000..3e3c84b9ce --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java @@ -0,0 +1,21 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestAnalysisChartRequest { + private boolean createCase; + private boolean updateCase; + private String order; + private List<Long> times; + private String startTime; + private String endTime; + private List<String> prioritys; + private List<String> projects; + private List<String> modules; + private List<String> users; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java new file mode 100644 index 0000000000..34bd70c583 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java @@ -0,0 +1,12 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TestAnalysisChartResult { + private String dateStr; + private String countNum; + +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java new file mode 100644 index 0000000000..e6e682ad79 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java @@ -0,0 +1,22 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestAnalysisResult { + private TestAnalysisChartDTO chartDTO; + private List<TestAnalysisTableDTO> tableDTOs; + + public TestAnalysisResult() { + + } + + public TestAnalysisResult(TestAnalysisChartDTO chartDTO, List<TestAnalysisTableDTO> tableDTOs) { + this.chartDTO = chartDTO; + this.tableDTOs = tableDTOs; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java new file mode 100644 index 0000000000..7f7a62d273 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java @@ -0,0 +1,29 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +public class TestAnalysisTableDTO { + private String id; + private String name; + private String createCount; + private String updateCount; + private List<TestAnalysisTableDTO> children; + + public TestAnalysisTableDTO() { + + } + + public TestAnalysisTableDTO(String name, String createCount, String updateCount, List<TestAnalysisTableDTO> children) { + this.id = UUID.randomUUID().toString(); + this.name = name; + this.createCount = createCount; + this.updateCount = updateCount; + this.children = children; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java new file mode 100644 index 0000000000..4c023d1428 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java @@ -0,0 +1,15 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TestCaseCountChartResult { + private String groupName; + private long countNum; + + public String getCountNumStr(){ + return String.valueOf(countNum); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java new file mode 100644 index 0000000000..227e2c0c1d --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java @@ -0,0 +1,79 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class TestCaseCountRequest { + //x轴字段 + private String xaxis; + //y轴字段 + private List<String> yaxis; + + //搜索条件 + private String projectId; + private String timeType; + private TimeFilter timeFilter; + private List<Long> times; + private String order; + + //起始时间 + private long startTime = 0; + //结束时间 + private long endTime = 0; + + //其余条件 + private String filterType; + private List<Map<String,Object>> filters; + + /** + * 功能用例、接口用例、场景用例、性能用例的分组字段 + */ + private String testCaseGroupColumn; + private String apiCaseGroupColumn; + private String scenarioCaseGroupColumn; + private String loadCaseGroupColumn; + + /** + * filter整理后的查询数据 + * @return + */ + private Map<String,List<String>> filterSearchList; + private Map<String,List<String>> apiFilterSearchList; + private Map<String,List<String>> loadFilterSearchList; + + public int getTimeRange(){ + if(timeFilter != null){ + return timeFilter.getTimeRange(); + }else { + return 0; + } + } + + public String getTimeRangeUnit(){ + if(timeFilter != null){ + return timeFilter.getTimeRangeUnit(); + }else { + return null; + } + } + + public void setFilterSearchList(String key,List<String> values){ + if(this.filterSearchList == null){ + this.filterSearchList = new HashMap<>(); + } + filterSearchList.put(key,values); + } +} + +@Getter +@Setter +class TimeFilter{ + private int timeRange; + private String timeRangeUnit; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java new file mode 100644 index 0000000000..70bfeaccab --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java @@ -0,0 +1,24 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestCaseCountResponse { + private TestAnalysisChartDTO barChartDTO; + private PieChartDTO pieChartDTO; + private List<TestCaseCountTableDTO> tableDTOs; + + public TestCaseCountResponse() { + + } + + public TestCaseCountResponse(TestAnalysisChartDTO chartDTO, PieChartDTO pieChartDTO, List<TestCaseCountTableDTO> tableDTOs) { + this.pieChartDTO = pieChartDTO; + this.barChartDTO = chartDTO; + this.tableDTOs = tableDTOs; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java new file mode 100644 index 0000000000..29f6042e28 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java @@ -0,0 +1,22 @@ +package io.metersphere.reportstatistics.dto; + +/** + * @author song.tianyang + * @Date 2021/9/8 5:36 下午 + */ +public class TestCaseCountSummary { + public String groupName; + + public long testCaseCount = 0; + public long apiCaseCount = 0; + public long scenarioCaseCount = 0; + public long loadCaseCount = 0; + + public TestCaseCountSummary(String groupName) { + this.groupName = groupName; + } + + public long getAllCount() { + return this.testCaseCount + this.apiCaseCount + this.scenarioCaseCount + this.loadCaseCount; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java new file mode 100644 index 0000000000..2ce5c86a2b --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java @@ -0,0 +1,34 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +public class TestCaseCountTableDTO { + private String id; + private String name; + private String allCount; + private String testCaseCount; + private String apiCaseCount; + private String scenarioCaseCount; + private String loadCaseCount; + + private List<TestCaseCountTableDTO> children; + + public TestCaseCountTableDTO(String name, long testCaseCount, long apiCaseCount, long scenarioCaseCount, long loadCaseCount) { + this.id = UUID.randomUUID().toString(); + this.name = name; + this.testCaseCount = String.valueOf(testCaseCount); + this.apiCaseCount = String.valueOf(apiCaseCount); + this.scenarioCaseCount = String.valueOf(scenarioCaseCount); + this.loadCaseCount = String.valueOf(loadCaseCount); + this.allCount = String.valueOf(testCaseCount+apiCaseCount+scenarioCaseCount+loadCaseCount); + + children = new ArrayList<>(); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java new file mode 100644 index 0000000000..5ac9f6545e --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java @@ -0,0 +1,20 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class Legend { + private final String x = "center"; + private final String y = "bottom"; + private final String type = "scroll"; + private final List<Integer> padding = Arrays.asList(0, 40, 0, 0); + private Map<String, Boolean> selected; + private List<String> data; + +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java new file mode 100644 index 0000000000..3222b624e5 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java @@ -0,0 +1,20 @@ +package io.metersphere.reportstatistics.dto.charts; + +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PieData { + private String name; + private long value; + private JSONObject itemStyle; + + public void setColor(String color){ + if(itemStyle == null){ + itemStyle = new JSONObject(); + } + itemStyle.put("color",color); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java new file mode 100644 index 0000000000..823e066a05 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java @@ -0,0 +1,20 @@ +package io.metersphere.reportstatistics.dto.charts; + +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class Series { + private String name; + private List<Object> data; + private String color = "#783887"; + private String type = "line"; + private String radius = "50"; + private String stack; + private JSONObject encode; + private List<String> center; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java new file mode 100644 index 0000000000..f79d210710 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java @@ -0,0 +1,14 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Title { + private String name; + private String subtext; + private String left; + private String top = "75%"; + private String textAlign = "center"; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java new file mode 100644 index 0000000000..543a3488ac --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java @@ -0,0 +1,17 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class XAxis { + private final String type = "category"; + private List<String> data; + private String name; + private Map<String,Integer> axisLabel = new HashMap<String,Integer>(){ {this.put("interval",0);this.put("rotate",30);}}; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java new file mode 100644 index 0000000000..30b70d9b31 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java @@ -0,0 +1,14 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class YAxis { + private String type; + private List<String> data; + private String name; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java b/backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java new file mode 100644 index 0000000000..f2e1e71c38 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java @@ -0,0 +1,77 @@ +package io.metersphere.reportstatistics.service; + +import io.metersphere.base.domain.ReportStatistics; +import io.metersphere.base.domain.ReportStatisticsExample; +import io.metersphere.base.domain.ReportStatisticsWithBLOBs; +import io.metersphere.base.mapper.ReportStatisticsMapper; +import io.metersphere.commons.utils.SessionUtils; +import io.metersphere.reportstatistics.dto.ReportStatisticsSaveRequest; +import io.metersphere.reportstatistics.dto.ReportStatisticsType; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.UUID; + +/** + * @author song.tianyang + * @Date 2021/9/14 4:50 下午 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class ReportStatisticsService { + @Resource + private ReportStatisticsMapper reportStatisticsMapper; + + public ReportStatisticsWithBLOBs saveByRequest(ReportStatisticsSaveRequest request) { + ReportStatisticsWithBLOBs model = new ReportStatisticsWithBLOBs(); + model.setId(UUID.randomUUID().toString()); + String name = "用例分析报表"; + if(StringUtils.equalsIgnoreCase(ReportStatisticsType.TEST_CASE_COUNT.name(),request.getReportType())){ + name = "用例统计报表"; + model.setReportType(ReportStatisticsType.TEST_CASE_COUNT.name()); + }else { + model.setReportType(ReportStatisticsType.TEST_CASE_ANALYSIS.name()); + } + model.setName(name); + model.setDataOption(request.getDataOption()); + model.setSelectOption(request.getSelectOption()); + model.setCreateTime(System.currentTimeMillis()); + model.setUpdateTime(System.currentTimeMillis()); + model.setProjectId(request.getProjectId()); + String userId = SessionUtils.getUserId(); + model.setCreateUser(userId); + model.setUpdateUser(userId); + + reportStatisticsMapper.insert(model); + + return model; + } + + public int deleteById(String id) { + return reportStatisticsMapper.deleteByPrimaryKey(id); + } + + public List<ReportStatistics> selectByProjectIdAndReportType(String projectId, String reportType) { + ReportStatisticsExample example = new ReportStatisticsExample(); + example.createCriteria().andProjectIdEqualTo(projectId).andReportTypeEqualTo(reportType); + example.setOrderByClause("create_time DESC"); + return reportStatisticsMapper.selectByExample(example); + } + + public ReportStatisticsWithBLOBs selectById(String id) { + return reportStatisticsMapper.selectByPrimaryKey(id); + } + + public ReportStatisticsWithBLOBs updateByRequest(ReportStatisticsSaveRequest request) { + ReportStatisticsWithBLOBs updateModel = new ReportStatisticsWithBLOBs(); + updateModel.setId(request.getId()); + updateModel.setName(request.getName()); + updateModel.setUpdateTime(request.getUpdateTime()); + updateModel.setUpdateUser(SessionUtils.getUserId()); + reportStatisticsMapper.updateByPrimaryKeySelective(updateModel); + return updateModel; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java b/backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java new file mode 100644 index 0000000000..aebd939897 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java @@ -0,0 +1,171 @@ +package io.metersphere.reportstatistics.service; + +import io.metersphere.base.mapper.ext.ExtTestAnalysisMapper; +import io.metersphere.commons.utils.DateUtils; +import io.metersphere.commons.utils.SessionUtils; +import io.metersphere.controller.request.ProjectRequest; +import io.metersphere.dto.ProjectDTO; +import io.metersphere.reportstatistics.dto.*; +import io.metersphere.reportstatistics.dto.charts.Legend; +import io.metersphere.reportstatistics.dto.charts.Series; +import io.metersphere.reportstatistics.dto.charts.XAxis; +import io.metersphere.reportstatistics.dto.charts.YAxis; +import io.metersphere.service.ProjectService; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional(rollbackFor = Exception.class) +public class TestAnalysisService { + @Resource + private ExtTestAnalysisMapper extTestAnalysisMapper; + @Resource + private ProjectService projectService; + + private final String ADD = "新增用例"; + private final String UPDATE = "修改用例"; + + public TestAnalysisResult getReport(TestAnalysisChartRequest request) { + if (CollectionUtils.isEmpty(request.getTimes())) { + // 最近七天 + request.setTimes(Arrays.asList(System.currentTimeMillis() - 7 * 24 * 3600 * 1000L, System.currentTimeMillis())); + } + request.setStartTime(DateUtils.getDataStr(request.getTimes().get(0))); + request.setEndTime(DateUtils.getDataStr(request.getTimes().get(1))); + if (CollectionUtils.isEmpty(request.getProjects())) { + // 获取当前组织空间下所有项目 + String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId(); + ProjectRequest projectRequest = new ProjectRequest(); + projectRequest.setWorkspaceId(currentWorkspaceId); + List<ProjectDTO> projectDTOS = projectService.getProjectList(projectRequest); + if (CollectionUtils.isNotEmpty(projectDTOS)) { + request.setProjects(projectDTOS.stream().map(ProjectDTO::getId).collect(Collectors.toList())); + } else { + request.setProjects(new LinkedList<String>(){{this.add(UUID.randomUUID().toString());}}); + } + } + TestAnalysisChartDTO dto = new TestAnalysisChartDTO(); + List<TestAnalysisTableDTO> dtos = new LinkedList<>(); + + List<Series> seriesList = new LinkedList<>(); + XAxis xAxis = new XAxis(); + + if (CollectionUtils.isEmpty(request.getUsers())) { + // 组织charts格式数据 + Legend legend = new Legend(); + formatLegend(legend, null, request); + dto.setLegend(legend); + List<TestAnalysisChartResult> createResults = extTestAnalysisMapper.getCraeteCaseReport(request); + // 获取修改的用例统计报表 + List<TestAnalysisChartResult> updateResults = extTestAnalysisMapper.getUpdateCaseReport(request); + formatXaxisSeries(xAxis, seriesList, "", dto, createResults, updateResults); + formatTable(dtos, createResults, updateResults); + } else { + List<String> users = request.getUsers(); + Legend legend = new Legend(); + formatLegend(legend, users, request); + dto.setLegend(legend); + + // 按用户展示 + boolean isFlag = true; + for (String item : users) { + request.setUsers(Arrays.asList(item)); + List<TestAnalysisChartResult> createResults = extTestAnalysisMapper.getCraeteCaseReport(request); + // 获取修改的用例统计报表 + List<TestAnalysisChartResult> updateResults = extTestAnalysisMapper.getUpdateCaseReport(request); + formatXaxisSeries(xAxis, seriesList, item + "-", dto, createResults, updateResults); + + // 初始化列表总量,按天统计总量 + if (isFlag) { + formatTable(dtos, createResults, updateResults); + isFlag = false; + } + // 增加子项 + for (int j = 0; j < dtos.size(); j++) { + TestAnalysisTableDTO childItem = new TestAnalysisTableDTO(item, createResults.get(j).getCountNum(), updateResults.get(j).getCountNum(), null); + dtos.get(j).getChildren().add(childItem); + } + } + } + // 每行总计 + dtos.forEach(item -> { + if (CollectionUtils.isNotEmpty(item.getChildren())) { + // table 总和计算 + List<Integer> collect = item.getChildren().stream().map(childItem -> Integer.valueOf(childItem.getCreateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> createCount = collect.stream().reduce(Integer::sum); + List<Integer> upCollect = item.getChildren().stream().map(childItem -> Integer.valueOf(childItem.getUpdateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> updateCount = upCollect.stream().reduce(Integer::sum); + item.setCreateCount(createCount.get().toString()); + item.setUpdateCount(updateCount.get().toString()); + } + }); + // table 总和计算 + List<Integer> collect = dtos.stream().map(item -> Integer.valueOf(item.getCreateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> createCount = collect.stream().reduce(Integer::sum); + List<Integer> upCollect = dtos.stream().map(item -> Integer.valueOf(item.getUpdateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> updateCount = upCollect.stream().reduce(Integer::sum); + dtos.add(new TestAnalysisTableDTO("总计", createCount.get().toString(), updateCount.get().toString(), new LinkedList<>())); + + TestAnalysisResult testAnalysisResult = new TestAnalysisResult(); + testAnalysisResult.setChartDTO(dto); + testAnalysisResult.setTableDTOs(dtos); + return testAnalysisResult; + } + + private void formatXaxisSeries(XAxis xAxis, List<Series> seriesList, String name, TestAnalysisChartDTO dto, List<TestAnalysisChartResult> createResults, List<TestAnalysisChartResult> updateResults) { + if (CollectionUtils.isNotEmpty(createResults)) { + xAxis.setData(createResults.stream().map(TestAnalysisChartResult::getDateStr).collect(Collectors.toList())); + Series series = new Series(); + series.setName(name + ADD); + series.setData(createResults.stream().map(TestAnalysisChartResult::getCountNum).collect(Collectors.toList())); + seriesList.add(series); + } + if (CollectionUtils.isNotEmpty(updateResults)) { + xAxis.setData(updateResults.stream().map(TestAnalysisChartResult::getDateStr).collect(Collectors.toList())); + Series series = new Series(); + series.setName(name + UPDATE); + series.setColor("#B8741A"); + series.setData(updateResults.stream().map(TestAnalysisChartResult::getCountNum).collect(Collectors.toList())); + seriesList.add(series); + } + dto.setXAxis(xAxis); + dto.setYAxis(new YAxis()); + dto.setSeries(seriesList); + } + + private void formatLegend(Legend legend, List<String> datas, TestAnalysisChartRequest request) { + Map<String, Boolean> selected = new LinkedHashMap<>(); + List<String> list = new LinkedList<>(); + if (CollectionUtils.isEmpty(datas)) { + selected.put(ADD, request.isCreateCase()); + selected.put(UPDATE, request.isUpdateCase()); + list.add(ADD); + list.add(UPDATE); + } else { + datas.forEach(item -> { + selected.put(item + "-" + ADD, request.isCreateCase()); + selected.put(item + "-" + UPDATE, request.isUpdateCase()); + list.add(item + "-" + ADD); + list.add(item + "-" + UPDATE); + }); + } + legend.setSelected(selected); + legend.setData(list); + } + + private void formatTable(List<TestAnalysisTableDTO> dtos, List<TestAnalysisChartResult> createResults, List<TestAnalysisChartResult> updateResults) { + for (int i = 0; i < createResults.size(); i++) { + TestAnalysisTableDTO dto = new TestAnalysisTableDTO(createResults.get(i).getDateStr(), createResults.get(i).getCountNum(), updateResults.get(i).getCountNum(), new LinkedList<>()); + dtos.add(dto); + } + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java b/backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java new file mode 100644 index 0000000000..1f438af677 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java @@ -0,0 +1,775 @@ +package io.metersphere.reportstatistics.service; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.base.domain.CustomField; +import io.metersphere.base.domain.User; +import io.metersphere.base.mapper.ext.ExtTestCaseCountMapper; +import io.metersphere.commons.utils.CommonBeanFactory; +import io.metersphere.commons.utils.DateUtils; +import io.metersphere.controller.request.member.QueryMemberRequest; +import io.metersphere.dto.TestCaseTemplateDao; +import io.metersphere.i18n.Translator; +import io.metersphere.reportstatistics.dto.*; +import io.metersphere.reportstatistics.dto.charts.*; +import io.metersphere.service.TestCaseTemplateService; +import io.metersphere.service.UserService; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; + +@Service +@Transactional(rollbackFor = Exception.class) +public class TestCaseCountService { + @Resource + private ExtTestCaseCountMapper extTestCaseCountMapper; + @Resource + UserService userService; + + public TestCaseCountResponse getReport(TestCaseCountRequest request) { + request.setFilterType(request.getFilterType().toUpperCase(Locale.ROOT)); + + TestAnalysisChartDTO dto = new TestAnalysisChartDTO(); + PieChartDTO pieChartDTO = new PieChartDTO(); + List<TestCaseCountTableDTO> dtos = new LinkedList<>(); + + List<Series> seriesList = new LinkedList<>(); + XAxis xAxis = new XAxis(); + xAxis.setAxisLabel(new HashMap<String,Integer>(){ {this.put("interval",0);this.put("rotate",0);}}); + + // 组织charts格式数据 + Legend legend = new Legend(); + formatLegend(legend, request.getYaxis(), request); + dto.setLegend(legend); + //根据X轴(分组计算字段)来整理不同表对应的字段 注意:x轴维护人/查询条件有维护人时 不查接口和性能; x轴为用例等级/查询条件有用例等级的,不查性能 + + boolean yAxisSelectTestCase = false; + boolean yAxisSelectApi = false; + boolean yAxisSelectScenarioCase = false; + boolean yAxisSelectLoad = false; + + boolean selectApi = true; + boolean selectLoad = true; + + boolean parseUser = false; + boolean parseStatus = false; + + switch (request.getXaxis()) { + case "creator": + request.setTestCaseGroupColumn("create_user"); + request.setApiCaseGroupColumn("create_user_id"); + request.setScenarioCaseGroupColumn("create_user"); + request.setLoadCaseGroupColumn("create_user"); + parseUser = true; + break; + case "maintainer": + request.setTestCaseGroupColumn("maintainer"); + request.setApiCaseGroupColumn("'无维护人'"); + request.setScenarioCaseGroupColumn("principal"); + request.setLoadCaseGroupColumn("'无维护人'"); + selectApi = false; + selectLoad = false; + parseUser = true; + break; + case "casetype": + Map<String, String> caseDescMap = this.getCaseDescMap(); + request.setTestCaseGroupColumn("'" + caseDescMap.get("testCaseDesc") + "'"); + request.setApiCaseGroupColumn("'" + caseDescMap.get("apiCaseDesc") + "'"); + request.setScenarioCaseGroupColumn("'" + caseDescMap.get("scenarioCaseDesc") + "'"); + request.setLoadCaseGroupColumn("'" + caseDescMap.get("loadCaseDesc") + "'"); + break; + case "casestatus": + request.setTestCaseGroupColumn("status"); + request.setApiCaseGroupColumn("status"); + request.setScenarioCaseGroupColumn("status"); + request.setLoadCaseGroupColumn("status"); + selectApi = false; + parseStatus = true; + break; + case "caselevel": + request.setTestCaseGroupColumn("priority"); + request.setApiCaseGroupColumn("priority"); + request.setScenarioCaseGroupColumn("level"); + request.setLoadCaseGroupColumn("'无用例等级'"); + selectLoad = false; + break; + default: + return new TestCaseCountResponse(); + } + + //计算时间 + if (StringUtils.equalsIgnoreCase(request.getTimeType(), "dynamicTime")) { + int dateCountType = 0; + if (StringUtils.equalsIgnoreCase(request.getTimeRangeUnit(), "day")) { + dateCountType = Calendar.DAY_OF_MONTH; + } else if (StringUtils.equalsIgnoreCase(request.getTimeRangeUnit(), "month")) { + dateCountType = Calendar.MONTH; + } else if (StringUtils.equalsIgnoreCase(request.getTimeRangeUnit(), "year")) { + dateCountType = Calendar.YEAR; + } + + if (dateCountType != 0 && request.getTimeRange() != 0) { + long startTime = DateUtils.dateSum(new Date(), (0 - request.getTimeRange()), dateCountType).getTime(); + request.setStartTime(startTime); + } + + } else if (StringUtils.equalsIgnoreCase(request.getTimeType(), "fixedTime")) { + if (CollectionUtils.isNotEmpty(request.getTimes()) && request.getTimes().size() == 2) { + request.setStartTime(request.getTimes().get(0)); + request.setEndTime(request.getTimes().get(1)); + } + } + + //计算更多属性 + if (CollectionUtils.isNotEmpty(request.getFilters())) { + for (Map<String, Object> filterMap : request.getFilters()) { + String filterType = String.valueOf(filterMap.get("type")); + + if (StringUtils.equalsAnyIgnoreCase(filterType, "casetype", "caselevel", "creator", "maintainer")) { + Object valueObj = filterMap.get("values"); + if (valueObj instanceof List) { + List<String> searchList = (List) valueObj; + if(!searchList.isEmpty()){ + request.setFilterSearchList(filterType, searchList); + } + } + + if (StringUtils.equalsIgnoreCase(filterType, "caselevel")) { + selectLoad = false; + }else if (StringUtils.equalsIgnoreCase(filterType, "maintainer")) { + selectApi = false; + selectLoad = false; + } + }else if(StringUtils.equalsAnyIgnoreCase(filterType, "casestatus")){ + List<String> searchList = new ArrayList<>(); + Object valueObj = filterMap.get("values"); + if (valueObj instanceof List) { + for (String statusStr : (List<String>) valueObj) { + searchList.add(statusStr.toUpperCase(Locale.ROOT)); + } + } + //如果包含Running + if(searchList.contains("RUNNING")){ + if(!searchList.contains("Starting")){ + searchList.add("STARTING"); + } + if(!searchList.contains("Underway")){ + searchList.add("UNDERWAY"); + } + } + + if(searchList.contains("FINISHED")){ + if(!searchList.contains("Completed")){ + searchList.add("Completed"); + } + } + + if(!searchList.isEmpty()){ + request.setFilterSearchList(filterType, searchList); + } + selectApi = false; + } + } + } + + //获取测试用例、接口用例、场景用例、性能用例的统计 + List<TestCaseCountChartResult> functionCaseCountResult = new ArrayList<>(); + List<TestCaseCountChartResult> apiCaseCountResult = new ArrayList<>(); + List<TestCaseCountChartResult> scenarioCaseCount = new ArrayList<>(); + List<TestCaseCountChartResult> loadCaseCount = new ArrayList<>(); + + List<String> moreOptionsAboutCaseType = new ArrayList<>(); + if (StringUtils.equalsIgnoreCase(request.getFilterType(), "And") && MapUtils.isNotEmpty(request.getFilterSearchList())) { + if (request.getFilterSearchList().containsKey("maintainer")) { + selectApi = false; + } + if (request.getFilterSearchList().containsKey("caselevel")) { + selectLoad = false; + } + + if (request.getFilterSearchList().containsKey("casetype")) { + //如果"且"查询,同时针对案例类型做过筛选,那么则分开批量查询 + List<String> selectCaseTypeList = request.getFilterSearchList().get("casetype"); + request.getFilterSearchList().remove("casetype"); + if (CollectionUtils.isNotEmpty(selectCaseTypeList)) { + moreOptionsAboutCaseType.addAll(selectCaseTypeList); + } + } + } + //没有选择的话默认搜索条件是所有类型的案例 + if(moreOptionsAboutCaseType.isEmpty()){ + moreOptionsAboutCaseType.add("testCase"); + moreOptionsAboutCaseType.add("apiCase"); + moreOptionsAboutCaseType.add("scenarioCase"); + moreOptionsAboutCaseType.add("loadCase"); + } + + //解析Y轴,判断要查询的案例类型 + if(CollectionUtils.isNotEmpty(request.getYaxis())){ + + for (String selectType:request.getYaxis()) { + if(moreOptionsAboutCaseType.contains(selectType)){ + if (StringUtils.equalsIgnoreCase(selectType, "testCase")) { + yAxisSelectTestCase = true; + } else if (StringUtils.equalsIgnoreCase(selectType, "apiCase")) { + if(selectApi){ + yAxisSelectApi = true; + } + } else if (StringUtils.equalsIgnoreCase(selectType, "scenarioCase")) { + yAxisSelectScenarioCase = true; + } else if (StringUtils.equalsIgnoreCase(selectType, "loadCase")) { + if(selectLoad){ + yAxisSelectLoad = true; + } + } + } + } + } + + + if(yAxisSelectTestCase){ + functionCaseCountResult = extTestCaseCountMapper.getFunctionCaseCount(request); + } + if (yAxisSelectApi) { + Map<String,List<String>> apiCaseFilterList = new HashMap<>(); + if(MapUtils.isNotEmpty(request.getFilterSearchList())){ + for (Map.Entry<String,List<String>> entry : request.getFilterSearchList().entrySet()) { + String type = entry.getKey(); + if(!StringUtils.equalsAnyIgnoreCase(type,"maintainer","casestatus")){ + apiCaseFilterList.put(entry.getKey(),entry.getValue()); + } + } + } + request.setApiFilterSearchList(apiCaseFilterList); + apiCaseCountResult = extTestCaseCountMapper.getApiCaseCount(request); + } + if(yAxisSelectScenarioCase){ + scenarioCaseCount = extTestCaseCountMapper.getScenarioCaseCount(request); + } + if (yAxisSelectLoad) { + Map<String,List<String>> loadCaseFilterMap = new HashMap<>(); + if(MapUtils.isNotEmpty(request.getFilterSearchList())){ + for (Map.Entry<String,List<String>> entry : request.getFilterSearchList().entrySet()) { + String type = entry.getKey(); + if(!StringUtils.equalsAnyIgnoreCase(type,"maintainer","caselevel")){ + loadCaseFilterMap.put(entry.getKey(),entry.getValue()); + } + } + } + request.setLoadFilterSearchList(loadCaseFilterMap); + loadCaseCount = extTestCaseCountMapper.getLoadCaseCount(request); + } + + Map<String, TestCaseCountSummary> summaryMap = this.summaryCountResult(parseUser, parseStatus,request.getProjectId(),request.getOrder(), + functionCaseCountResult, apiCaseCountResult, scenarioCaseCount, loadCaseCount); + + formatXaxisSeries(xAxis, seriesList, dto, summaryMap); + formatTable(dtos, summaryMap); + + formatPieChart(pieChartDTO, request.getXaxis(), summaryMap,yAxisSelectTestCase,yAxisSelectApi,yAxisSelectScenarioCase,yAxisSelectLoad); + + TestCaseCountResponse testCaseCountResult = new TestCaseCountResponse(); + testCaseCountResult.setBarChartDTO(dto); + testCaseCountResult.setTableDTOs(dtos); + testCaseCountResult.setPieChartDTO(pieChartDTO); + return testCaseCountResult; + } + + private void formatPieChart(PieChartDTO pieChartDTO, String groupName, Map<String, TestCaseCountSummary> summaryMap, + boolean selectTestCase, boolean selectApi, boolean selectScenarioCase, boolean selectLoad) { + JSONArray titleArray = new JSONArray(); + titleArray.add("type"); + titleArray.add("count"); + titleArray.add(groupName); + + List<Series> seriesArr = new ArrayList<>(); + List<Title> titles = new ArrayList<>(); + + int leftPx = 200; + Map<String, String> caseDescMap = this.getCaseDescMap(); + for (TestCaseCountSummary summary : summaryMap.values()) { + String leftPxStr = String.valueOf(leftPx); + + List<Object> dataList = new ArrayList<>(); + + if(selectTestCase && summary.testCaseCount > 0){ + PieData pieData = new PieData(); + pieData.setName(caseDescMap.get("testCaseDesc")); + pieData.setValue(summary.testCaseCount); + pieData.setColor("#F38F1F"); + dataList.add(pieData); + } + + if(selectApi && summary.apiCaseCount > 0){ + PieData apicasePieData = new PieData(); + apicasePieData.setName(caseDescMap.get("apiCaseDesc")); + apicasePieData.setValue(summary.apiCaseCount); + apicasePieData.setColor("#6FD999"); + dataList.add(apicasePieData); + } + + if(selectScenarioCase && summary.scenarioCaseCount > 0){ + PieData scenarioPieData = new PieData(); + scenarioPieData.setName(caseDescMap.get("scenarioCaseDesc")); + scenarioPieData.setValue(summary.scenarioCaseCount); + scenarioPieData.setColor("#2884F3"); + dataList.add(scenarioPieData); + } + + if(selectLoad && summary.loadCaseCount > 0){ + PieData loadCasePieData = new PieData(); + loadCasePieData.setName(caseDescMap.get("loadCaseDesc")); + loadCasePieData.setValue(summary.loadCaseCount); + loadCasePieData.setColor("#F45E53"); + dataList.add(loadCasePieData); + } + + Series series = new Series(); + series.setType("pie"); + series.setRadius("50"); + series.setEncode(new JSONObject() {{ + this.put("itemName", "groupname"); + this.put("value", summary.groupName); + }}); + seriesArr.add(series); + + series.setData(dataList); + series.setCenter(new ArrayList<String>() {{ + this.add(leftPxStr); + this.add("50%"); + }}); + + Title title = new Title(); + title.setSubtext(summary.groupName); + title.setLeft(leftPxStr); + titles.add(title); + + leftPx = leftPx + 350; + } + + pieChartDTO.setSeries(seriesArr); + pieChartDTO.setTitle(titles); + pieChartDTO.setWidth(leftPx); + } + + private Map<String, TestCaseCountSummary> summaryCountResult(boolean parseGroupNameToUserName, boolean parseGrouNameToCaseStatus, String projectId, String order, + List<TestCaseCountChartResult> functionCaseCountResult, List<TestCaseCountChartResult> apiCaseCountResult, List<TestCaseCountChartResult> scenarioCaseCount, List<TestCaseCountChartResult> loadCaseCount) { + Map<String, TestCaseCountSummary> summaryMap = new LinkedHashMap<>(); + + //groupName 解析对象 + Map<String, String> groupNameParseMap = new HashMap<>(); + if (parseGroupNameToUserName) { + groupNameParseMap.putAll(this.getUserIdMap()); + } + if (parseGrouNameToCaseStatus) { + groupNameParseMap.putAll(this.getCaseStatusMap(projectId)); + } + + if (CollectionUtils.isNotEmpty(functionCaseCountResult)) { + for (TestCaseCountChartResult result : functionCaseCountResult) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.testCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + if (CollectionUtils.isNotEmpty(apiCaseCountResult)) { + for (TestCaseCountChartResult result : apiCaseCountResult) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.apiCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + if (CollectionUtils.isNotEmpty(scenarioCaseCount)) { + for (TestCaseCountChartResult result : scenarioCaseCount) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.scenarioCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + if (CollectionUtils.isNotEmpty(loadCaseCount)) { + for (TestCaseCountChartResult result : loadCaseCount) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.loadCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + Map<String, TestCaseCountSummary> returmMap = new LinkedHashMap<>(); + + if(StringUtils.equalsIgnoreCase(order,"desc")){ + TreeMap<Long,List<TestCaseCountSummary>> treeMap = new TreeMap<>(); + for (TestCaseCountSummary model : summaryMap.values()) { + if(treeMap.containsKey(model.getAllCount())){ + treeMap.get(model.getAllCount()).add(model); + }else { + List<TestCaseCountSummary> list = new ArrayList<>(); + list.add(model); + treeMap.put(model.getAllCount(),list); + } + } + ArrayList<TestCaseCountSummary> sortedList = new ArrayList<>(); + for (List<TestCaseCountSummary> list : treeMap.values()) { + sortedList.addAll(list); + } + + for (int i = sortedList.size(); i > 0; i --) { + TestCaseCountSummary model = sortedList.get(i-1); + returmMap.put(model.groupName,model); + } + }else if(StringUtils.equalsIgnoreCase(order,"asc")){ + TreeMap<Long,List<TestCaseCountSummary>> treeMap = new TreeMap<>(); + for (TestCaseCountSummary model : summaryMap.values()) { + if(treeMap.containsKey(model.getAllCount())){ + treeMap.get(model.getAllCount()).add(model); + }else { + List<TestCaseCountSummary> list = new ArrayList<>(); + list.add(model); + treeMap.put(model.getAllCount(),list); + } + } + for (List<TestCaseCountSummary> list : treeMap.values()) { + for (TestCaseCountSummary model : list ) { + returmMap.put(model.groupName,model); + } + } + }else { + returmMap = summaryMap; + } + + + return returmMap; + } + + private Map<String, String> getUserIdMap() { + List<User> userList = userService.getUserList(); + Map<String, String> userIdMap = new HashMap<>(); + for (User model : userList) { + userIdMap.put(model.getId(), model.getId() + "\n(" + model.getName() + ")"); + } + return userIdMap; + } + + private Map<String, String> getCaseStatusMap(String projectId) { + + Map<String, String> caseStatusMap = new HashMap<>(); + + TestCaseTemplateService testCaseTemplateService = CommonBeanFactory.getBean(TestCaseTemplateService.class); + TestCaseTemplateDao testCaseTemplate = testCaseTemplateService.getTemplate(projectId); + + caseStatusMap.put("prepare", Translator.get("test_case_status_prepare")); + caseStatusMap.put("error", Translator.get("test_case_status_error")); + caseStatusMap.put("success", Translator.get("test_case_status_success")); + caseStatusMap.put("trash", Translator.get("test_case_status_trash")); + caseStatusMap.put("underway", Translator.get("test_case_status_running")); + caseStatusMap.put("starting", Translator.get("test_case_status_running")); + caseStatusMap.put("saved", Translator.get("test_case_status_saved")); + caseStatusMap.put("running", Translator.get("test_case_status_running")); + caseStatusMap.put("finished", Translator.get("test_case_status_finished")); + caseStatusMap.put("completed", Translator.get("test_case_status_finished")); + + if (testCaseTemplate != null && CollectionUtils.isNotEmpty(testCaseTemplate.getCustomFields())) { + for (CustomField customField : testCaseTemplate.getCustomFields()) { + if (StringUtils.equals(customField.getName(), "用例状态")) { + JSONArray optionsArr = JSONArray.parseArray(customField.getOptions()); + for (int i = 0; i < optionsArr.size(); i++) { + JSONObject jsonObject = optionsArr.getJSONObject(i); + if (jsonObject.containsKey("value") && jsonObject.containsKey("text") && + !StringUtils.equalsAnyIgnoreCase(jsonObject.getString("value"), "Prepare", "Error", "Success", "Trash", "Underway", "Starting", "Saved")) { + caseStatusMap.put(jsonObject.getString("value"), jsonObject.getString("text")); + } + } + } + } + } + + return caseStatusMap; + } + + + private void formatXaxisSeries(XAxis xAxis, List<Series> seriesList, TestAnalysisChartDTO dto, + Map<String, TestCaseCountSummary> summaryMap) { + List<String> xAxisDataList = new ArrayList<>(); + + List<Object> testCaseCountList = new ArrayList<>(); + List<Object> apiCaseCountList = new ArrayList<>(); + List<Object> scenarioCaseCountList = new ArrayList<>(); + List<Object> loadCaseCountList = new ArrayList<>(); + for (TestCaseCountSummary summary : summaryMap.values()) { + xAxisDataList.add(summary.groupName); + testCaseCountList.add(String.valueOf(summary.testCaseCount)); + apiCaseCountList.add(String.valueOf(summary.apiCaseCount)); + scenarioCaseCountList.add(String.valueOf(summary.scenarioCaseCount)); + loadCaseCountList.add(String.valueOf(summary.loadCaseCount)); + } + xAxis.setData(xAxisDataList); + + Map<String, String> caseDescMap = this.getCaseDescMap(); + + Series tetcaseSeries = new Series(); + tetcaseSeries.setName(caseDescMap.get("testCaseDesc")); + tetcaseSeries.setColor("#F38F1F"); + tetcaseSeries.setRadius("20%"); + tetcaseSeries.setType("bar"); + tetcaseSeries.setStack("total"); + tetcaseSeries.setData(testCaseCountList); + seriesList.add(tetcaseSeries); + + Series apiSeries = new Series(); + apiSeries.setName(caseDescMap.get("apiCaseDesc")); + apiSeries.setColor("#6FD999"); + apiSeries.setType("bar"); + apiSeries.setStack("total"); + apiSeries.setData(apiCaseCountList); + seriesList.add(apiSeries); + + Series scenarioSeries = new Series(); + scenarioSeries.setName(caseDescMap.get("scenarioCaseDesc")); + scenarioSeries.setColor("#2884F3"); + scenarioSeries.setType("bar"); + scenarioSeries.setStack("total"); + scenarioSeries.setData(scenarioCaseCountList); + seriesList.add(scenarioSeries); + + Series loadSeries = new Series(); + loadSeries.setName(caseDescMap.get("loadCaseDesc")); + loadSeries.setColor("#F45E53"); + loadSeries.setType("bar"); + loadSeries.setStack("total"); + loadSeries.setData(loadCaseCountList); + seriesList.add(loadSeries); + + dto.setXAxis(xAxis); + dto.setYAxis(new YAxis()); + dto.setSeries(seriesList); + } + + private void formatLegend(Legend legend, List<String> datas, TestCaseCountRequest yrequest) { + Map<String, Boolean> selected = new LinkedHashMap<>(); + List<String> list = new LinkedList<>(); + legend.setSelected(selected); + legend.setData(datas); + } + + private void formatTable(List<TestCaseCountTableDTO> dtos, Map<String, TestCaseCountSummary> summaryMap) { + for (TestCaseCountSummary summary : summaryMap.values()) { + TestCaseCountTableDTO dto = new TestCaseCountTableDTO(summary.groupName, summary.testCaseCount, summary.apiCaseCount, summary.scenarioCaseCount, summary.loadCaseCount); + dtos.add(dto); + } + } + + private Map<String, String> getCaseDescMap() { + Map<String, String> map = new HashMap<>(); + map.put("testCaseDesc", Translator.get("test_case")); + map.put("apiCaseDesc", Translator.get("api_case")); + map.put("scenarioCaseDesc", Translator.get("scenario_case")); + map.put("loadCaseDesc", Translator.get("performance_case")); + return map; + } + + public Map<String, List<Map<String, String>>> getSelectFilterDatas(String projectId) { + Map<String, List<Map<String, String>>> returnMap = new HashMap<>(); + + //组装用户 + QueryMemberRequest memberRequest = new QueryMemberRequest(); + memberRequest.setProjectId(projectId); + List<User> userList = userService.getUserList(); + + List<Map<String, String>> returnUserList = new ArrayList<>(); + for (User user : userList) { + Map<String, String> map = new HashMap<>(); + map.put("id", user.getId()); + map.put("label", user.getId() + "(" + user.getName() + ")"); + returnUserList.add(map); + } + + //组装用例等级和用例状态 + TestCaseTemplateService testCaseTemplateService = CommonBeanFactory.getBean(TestCaseTemplateService.class); + TestCaseTemplateDao testCaseTemplate = testCaseTemplateService.getTemplate(projectId); + + List<Map<String, String>> caseLevelList = new ArrayList<>(); + List<Map<String, String>> caseStatusList = new ArrayList<>(); + Map<String, String> statusMap1 = new HashMap<>(); + statusMap1.put("id", "Prepare"); + statusMap1.put("label", Translator.get("test_case_status_prepare")); + +// Map<String, String> statusMap2 = new HashMap<>(); +// statusMap2.put("id", "Error"); +// statusMap2.put("label", Translator.get("test_case_status_error")); +// +// Map<String, String> statusMap3 = new HashMap<>(); +// statusMap3.put("id", "Success"); +// statusMap3.put("label", Translator.get("test_case_status_success")); + +// Map<String, String> statusMap4 = new HashMap<>(); +// statusMap4.put("id", "Trash"); +// statusMap4.put("label", Translator.get("test_case_status_trash")); + +// Map<String, String> statusMap5 = new HashMap<>(); +// statusMap5.put("id", "Underway"); +// statusMap5.put("label", Translator.get("test_case_status_running")); + +// Map<String, String> statusMap6 = new HashMap<>(); +// statusMap6.put("id", "Starting"); +// statusMap6.put("label", Translator.get("test_case_status_running")); + + Map<String, String> statusMap7 = new HashMap<>(); + statusMap7.put("id", "Saved"); + statusMap7.put("label", Translator.get("test_case_status_saved")); + + Map<String, String> statusMap8 = new HashMap<>(); + statusMap8.put("id", "Running"); + statusMap8.put("label", Translator.get("test_case_status_running")); + + Map<String, String> statusMap9 = new HashMap<>(); + statusMap9.put("id", "Finished"); + statusMap9.put("label", Translator.get("test_case_status_finished")); + + caseStatusList.add(statusMap1); +// caseStatusList.add(statusMap2); +// caseStatusList.add(statusMap3); +// caseStatusList.add(statusMap4); +// caseStatusList.add(statusMap5); +// caseStatusList.add(statusMap6); + caseStatusList.add(statusMap7); + caseStatusList.add(statusMap8); + caseStatusList.add(statusMap9); + + Map<String, String> levelMap1 = new HashMap<>(); + levelMap1.put("id", "P0"); + levelMap1.put("label", "P0"); + Map<String, String> levelMap2 = new HashMap<>(); + levelMap2.put("id", "P1"); + levelMap2.put("label", "P1"); + Map<String, String> levelMap3 = new HashMap<>(); + levelMap3.put("id", "P2"); + levelMap3.put("label", "P2"); + Map<String, String> levelMap4 = new HashMap<>(); + levelMap4.put("id", "P3"); + levelMap4.put("label", "P3"); + caseLevelList.add(levelMap1); + caseLevelList.add(levelMap2); + caseLevelList.add(levelMap3); + caseLevelList.add(levelMap4); + + + if (testCaseTemplate != null && CollectionUtils.isNotEmpty(testCaseTemplate.getCustomFields())) { + for (CustomField customField : testCaseTemplate.getCustomFields()) { + if (StringUtils.equals(customField.getName(), "用例状态")) { + JSONArray optionsArr = JSONArray.parseArray(customField.getOptions()); + for (int i = 0; i < optionsArr.size(); i++) { + JSONObject jsonObject = optionsArr.getJSONObject(i); + if (jsonObject.containsKey("value") && jsonObject.containsKey("text")) { + String value = jsonObject.getString("value"); + if(!StringUtils.equalsAnyIgnoreCase(value, "Prepare", "Error", "Success", "Trash", "Underway", "Starting", "Saved", + "Completed","test_track.case.status_finished")){ + Map<String, String> statusMap = new HashMap<>(); + statusMap.put("id", jsonObject.getString("value")); + statusMap.put("label", jsonObject.getString("text")); + caseStatusList.add(statusMap); + } + } + } + } else if (StringUtils.equals(customField.getName(), "用例等级")) { + JSONArray optionsArr = JSONArray.parseArray(customField.getOptions()); + for (int i = 0; i < optionsArr.size(); i++) { + JSONObject jsonObject = optionsArr.getJSONObject(i); + if (jsonObject.containsKey("value") && jsonObject.containsKey("text") && + !StringUtils.equalsAnyIgnoreCase(jsonObject.getString("value"), "P0", "P1", "P2", "P3")) { + Map<String, String> levelMap = new HashMap<>(); + levelMap.put("id", jsonObject.getString("value")); + levelMap.put("label", jsonObject.getString("text")); + caseLevelList.add(levelMap); + } + } + } + } + } + Map<String, String> caseDescMap = this.getCaseDescMap(); + // 组装用例类型 + List<Map<String, String>> caseTypeList = new ArrayList<>(); + Map<String, String> caseTypeMap1 = new HashMap<>(); + caseTypeMap1.put("id", "testCase"); + caseTypeMap1.put("label", caseDescMap.get("testCaseDesc")); + Map<String, String> caseTypeMap2 = new HashMap<>(); + caseTypeMap2.put("id", "apiCase"); + caseTypeMap2.put("label", caseDescMap.get("apiCaseDesc")); + Map<String, String> caseTypeMap3 = new HashMap<>(); + caseTypeMap3.put("id", "scenarioCase"); + caseTypeMap3.put("label", caseDescMap.get("scenarioCaseDesc")); + Map<String, String> caseTypeMap4 = new HashMap<>(); + caseTypeMap4.put("id", "loadCase"); + caseTypeMap4.put("label", caseDescMap.get("loadCaseDesc")); + caseTypeList.add(caseTypeMap1); + caseTypeList.add(caseTypeMap2); + caseTypeList.add(caseTypeMap3); + caseTypeList.add(caseTypeMap4); + + returnMap.put("casetype", caseTypeList); + returnMap.put("caselevel", caseLevelList); + returnMap.put("casestatus", caseStatusList); + returnMap.put("creator", returnUserList); + returnMap.put("maintainer", returnUserList); + return returnMap; + } +} diff --git a/backend/src/main/java/io/metersphere/xpack b/backend/src/main/java/io/metersphere/xpack index bc8f05fdd7..6edae7aeeb 160000 --- a/backend/src/main/java/io/metersphere/xpack +++ b/backend/src/main/java/io/metersphere/xpack @@ -1 +1 @@ -Subproject commit bc8f05fdd7bfa0a13438d409394a2d150187f88b +Subproject commit 6edae7aeeb9d5ade65d64a115d00af96e4dc56d6 diff --git a/backend/src/main/resources/permission.json b/backend/src/main/resources/permission.json index c51c2f4e80..73024e68cd 100644 --- a/backend/src/main/resources/permission.json +++ b/backend/src/main/resources/permission.json @@ -816,6 +816,18 @@ "name": "导出", "resourceId": "PROJECT_REPORT_ANALYSIS", "license": true + }, + { + "id": "PROJECT_REPORT_ANALYSIS:READ+UPDATE", + "name": "保存", + "resourceId": "PROJECT_REPORT_ANALYSIS", + "license": true + }, + { + "id": "PROJECT_REPORT_ANALYSIS:READ+CREATE", + "name": "另存为", + "resourceId": "PROJECT_REPORT_ANALYSIS", + "license": true } ], "resource": [ diff --git a/frontend/src/business/components/common/router/router.js b/frontend/src/business/components/common/router/router.js index 312338f335..d3ff0889c9 100644 --- a/frontend/src/business/components/common/router/router.js +++ b/frontend/src/business/components/common/router/router.js @@ -5,11 +5,12 @@ import Setting from "@/business/components/settings/router"; import API from "@/business/components/api/router"; import Performance from "@/business/components/performance/router"; import Track from "@/business/components/track/router"; +import ReportStatistics from "@/business/components/reportstatistics/router"; import {getCurrentUserId} from "@/common/js/utils"; -const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/); -const Report = requireContext.keys().map(key => requireContext(key).report); -const ReportObj = Report && Report != null && Report.length > 0 && Report[0] != undefined ? Report : [{path: "/sidebar"}]; +// const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/); +// const Report = requireContext.keys().map(key => requireContext(key).report); +// const ReportObj = Report && Report != null && Report.length > 0 && Report[0] != undefined ? Report : [{path: "/sidebar"}]; Vue.use(VueRouter); @@ -26,7 +27,8 @@ const router = new VueRouter({ API, Performance, Track, - ...ReportObj + ReportStatistics, + // ...ReportStatistics ] }); diff --git a/frontend/src/business/components/common/select-tree/SelectTree.vue b/frontend/src/business/components/common/select-tree/SelectTree.vue index 73f42f9a05..5d61c9a383 100644 --- a/frontend/src/business/components/common/select-tree/SelectTree.vue +++ b/frontend/src/business/components/common/select-tree/SelectTree.vue @@ -290,11 +290,14 @@ export default { this.$refs.tree.setCheckedKeys(thisKeys); this.returnDataKeys = thisKeys; let t = []; + this.options = []; thisKeys.map((item) => {//设置option选项 let node = this.$refs.tree.getNode(item); // 所有被选中的节点对应的node - t.push(node.data); - this.options.push({label: node.label, value: node.key}); - return {label: node.label, value: node.key}; + if(node){ + t.push(node.data); + this.options.push({label: node.label, value: node.key}); + return {label: node.label, value: node.key}; + } }); this.returnDatas = t; this.popoverHide() @@ -398,6 +401,23 @@ export default { }, deep: true }, + defaultKey:{ + handler:function(){ + this.init(); + if(this.data && this.data.length > 0){ + this.$refs.tree.setCheckedKeys(this.defaultKey); + } + }, + deep:true + }, + data:{ + handler:function(){ + if(this.defaultKey && this.defaultKey.length > 0){ + this.$refs.tree.setCheckedKeys(this.defaultKey); + } + }, + deep:true + }, filterText(val) { this.$nextTick(() => { this.$refs.tree.filter(val); diff --git a/frontend/src/business/components/reportstatistics/ReportAnalysis.vue b/frontend/src/business/components/reportstatistics/ReportAnalysis.vue new file mode 100644 index 0000000000..c2cb0a8f98 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/ReportAnalysis.vue @@ -0,0 +1,101 @@ +<template> + <div> + <el-row type="flex"> + <p class="tip"> + <span class="ms-span">{{$t('commons.report_statistics.name')}}</span> + <el-select v-model="reportType" class="ms-col-type" size="mini" style="width: 120px"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in reportTypes"/> + </el-select> + </p> + </el-row> + <transition> + <keep-alive> + <report-card @openCard="openCard"/> + </keep-alive> + </transition> + + <!-- 测试用例趋势页面 --> + <ms-drawer :visible="testCaseTrendDrawer" :size="100" @close="close" direction="right" :show-full-screen="false" :is-show-close="false" style="overflow: hidden"> + <template v-slot:header> + <report-header :title="$t('commons.report_statistics.test_case_analysis')" :history-report-id="historyReportId" + @closePage="close" @saveReport="saveReport" @selectAndSaveReport="selectAndSaveReport"/> + </template> + <test-analysis-container @initHistoryReportId="initHistoryReportId" ref="testAnalysisContainer"/> + </ms-drawer> + + <!-- 测试用例分析页面 --> + <ms-drawer :visible="testCaseCountDrawer" :size="100" @close="close" direction="right" :show-full-screen="false" :is-show-close="false" style="overflow: hidden"> + <template v-slot:header> + <report-header :title="$t('commons.report_statistics.test_case_count')" :history-report-id="historyReportId" + @closePage="close" @saveReport="saveReport" @selectAndSaveReport="selectAndSaveReport"/> + </template> + <test-case-count-container @initHistoryReportId="initHistoryReportId" ref="testCaseCountContainer"/> + </ms-drawer> + </div> +</template> + +<script> + import ReportCard from "./ReportCard"; + import TestAnalysisContainer from "./track/TestAnalysisContainer"; + import MsDrawer from "@/business/components/common/components/MsDrawer"; + import ReportHeader from './base/ReportHeader'; + import TestCaseCountContainer from "./testCaseCount/TestCaseCountContainer"; + + export default { + name: "ReportAnalysis", + components: {ReportCard, TestAnalysisContainer, MsDrawer, ReportHeader, TestCaseCountContainer}, + data() { + return { + reportType: "track", + testCaseTrendDrawer: false, + testCaseCountDrawer: false, + historyReportId:"", + reportTypes: [{id: 'track', name: this.$t('test_track.test_track')}], + } + }, + methods: { + openCard(type) { + if(type === 'trackTestCase'){ + this.testCaseTrendDrawer = true; + }else if(type === 'countTestCase'){ + this.testCaseCountDrawer = true; + } + }, + close() { + this.testCaseTrendDrawer = false; + this.testCaseCountDrawer = false; + }, + saveReport(){ + if(this.testCaseTrendDrawer){ + this.$refs.testAnalysisContainer.saveReport(); + }else if(this.testCaseCountDrawer){ + this.$refs.testCaseCountContainer.saveReport(); + } + }, + selectAndSaveReport(){ + if(this.testCaseTrendDrawer){ + this.$refs.testAnalysisContainer.selectAndSaveReport(); + }else if(this.testCaseCountDrawer){ + this.$refs.testCaseCountContainer.selectAndSaveReport(); + } + }, + initHistoryReportId(reportId){ + this.historyReportId = reportId; + }, + }, + } +</script> + +<style scoped> + .ms-span { + margin: 10px 10px 0px + } + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 10px 20px 0px; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/ReportCard.vue b/frontend/src/business/components/reportstatistics/ReportCard.vue new file mode 100644 index 0000000000..648ad41b63 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/ReportCard.vue @@ -0,0 +1,138 @@ +<template> + <div class="ms-content"> + <el-row> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col" @click.native="openCard('trackTestCase')"> + <img src="../../../assets/track.jpg" class="image"> + <div style="padding: 10px;"> + <span>{{$t('commons.report_statistics.test_case_analysis')}}</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col" @click.native="openCard('countTestCase')"> + <img src="../../../assets/track.jpg" class="image"> + <div style="padding: 10px;"> + <span>{{$t('commons.report_statistics.test_case_count')}}</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_count_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + </el-row> + </div> +</template> + +<script> + import {hasPermission} from "@/common/js/utils"; + + export default { + name: "ReportCard", + components: {}, + data() { + return {} + }, + methods: { + openCard(type) { + if (!hasPermission('PROJECT_REPORT_ANALYSIS:READ')) { + this.$warning("无查看权限!"); + return; + } + this.$emit('openCard', type); + } + }, + } +</script> + +<style scoped> + .time { + font-size: 13px; + color: #999; + } + + .bottom { + margin-top: 13px; + line-height: 12px; + } + + .button { + padding: 0; + float: right; + } + + .image { + width: 100%; + display: block; + } + + .clearfix:before, + .clearfix:after { + display: table; + content: ""; + } + + .clearfix:after { + clear: both + } + + .ms-col { + margin: 5px; + } + + .ms-content { + padding: 15px 10px 15px 15px; + } + + .ms-col:hover { + cursor: pointer; + border-color: #783887; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/base/HistoryReportData.vue b/frontend/src/business/components/reportstatistics/base/HistoryReportData.vue new file mode 100644 index 0000000000..5922f31499 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/base/HistoryReportData.vue @@ -0,0 +1,86 @@ +<template> + <div> + <el-tabs v-model="activeName"> + <el-tab-pane :label="$t('commons.report_statistics.report_data.all_report')" name="allReport"> + <history-report-data-card :report-data="allReportData" :show-options-button="false" @deleteReport="deleteReport" @selectReport="selectReport"/> + </el-tab-pane> + <el-tab-pane :label="$t('commons.report_statistics.report_data.my_report')" name="myReport"> + <history-report-data-card :report-data="myReportData" :show-options-button="true" @deleteReport="deleteReport" @selectReport="selectReport"/> + </el-tab-pane> + </el-tabs> + </div> +</template> + +<script> + import {getCurrentProjectID,getCurrentUserId} from "@/common/js/utils"; + import HistoryReportDataCard from "./compose/HistoryReportDataCard"; + export default { + name: "HistoryReportData", + components: {HistoryReportDataCard}, + data() { + return { + activeName: 'allReport', + allReportData: [], + myReportData: [], + } + }, + props:{ + reportType:String + }, + created(){ + this.initReportData(); + }, + watch :{ + activeName(){ + this.initReportData(); + } + }, + methods: { + initReportData(){ + let projectId = getCurrentProjectID(); + let userId = getCurrentUserId(); + this.allReportData = []; + this.myReportData = []; + + let paramsObj = { + projectId:getCurrentProjectID(), + reportType:this.reportType, + }; + this.$post('/history/report/selectByParams',paramsObj, response => { + let allData = response.data; + allData.forEach(item => { + if(item){ + this.allReportData.push(item); + if(item.createUser === userId){ + this.myReportData.push(item); + } + } + }); + }); + }, + deleteReport(deleteId){ + let paramObj = { + id:deleteId + } + this.$post('/history/report/deleteByParam',paramObj, response => { + this.initReportData(); + }); + this.$emit("removeHistoryReportId"); + }, + selectReport(id){ + this.$emit("selectReport",id); + } + }, +} +</script> + +<style scoped> + +.historyCard{ + border: 0px; +} +/deep/ .el-card__header{ + border: 0px; +} + +</style> diff --git a/frontend/src/business/components/reportstatistics/base/ReportHeader.vue b/frontend/src/business/components/reportstatistics/base/ReportHeader.vue new file mode 100644 index 0000000000..c16290229d --- /dev/null +++ b/frontend/src/business/components/reportstatistics/base/ReportHeader.vue @@ -0,0 +1,115 @@ +<template> + <div class="ms-header"> + <el-row> + <div class="ms-div">{{title}}</div> + <div class="ms-header-right"> + <el-button type="primary" v-if="isSaveAsButtonShow" size="mini" @click="handleSaveAs" :disabled="readOnly">{{ $t('commons.save_as') }}<i class="el-icon-files el-icon--right"></i></el-button> + <el-button type="primary" v-if="isSaveButtonShow" size="mini" @click="handleSave" :disabled="readOnly">{{ $t('commons.save') }}<i class="el-icon-files el-icon--right"></i></el-button> + <el-button type="" size="mini" @click="handleExport" :disabled="readOnly">{{ $t('report.export') }}<i class="el-icon-download el-icon--right"></i></el-button> + <span class="ms-span">|</span> + <i class="el-icon-close report-alt-ico" @click="close"/> + </div> + </el-row> + </div> +</template> + +<script> +import {exportPdf, hasPermission} from "@/common/js/utils"; + import html2canvas from 'html2canvas'; + + export default { + name: "ReportHeader", + components: {}, + data() { + return {} + }, + props:{ + title:String, + historyReportId:String, + }, + created() { + }, + computed: { + readOnly() { + return !hasPermission('PROJECT_REPORT_ANALYSIS:READ+EXPORT'); + }, + isSaveAsButtonShow(){ + if(!this.historyReportId || this.historyReportId === null || this.historyReportId === ''){ + return false; + }else { + if(hasPermission('PROJECT_REPORT_ANALYSIS:READ+CREATE')){ + return true; + }else { + return false; + } + } + }, + isSaveButtonShow(){ + if(hasPermission('PROJECT_REPORT_ANALYSIS:READ+UPDATE')){ + return true; + }else { + return false; + } + } + }, + methods: { + handleExport() { + let name = this.title; + this.$nextTick(function () { + setTimeout(() => { + html2canvas(document.getElementById('reportAnalysis'), { + scale: 2 + }).then(function (canvas) { + exportPdf(name, [canvas]); + }); + }, 1000); + }); + }, + handleSave(){ + this.$emit("saveReport"); + }, + handleSaveAs(){ + this.$emit("selectAndSaveReport"); + }, + close() { + this.$emit('closePage'); + }, + }, + } +</script> + +<style scoped> + .ms-header { + border-bottom: 1px solid #E6E6E6; + background-color: #FFF; + } + + .ms-div { + float: left; + margin-left: 20px; + margin-top: 12px; + } + + .ms-span { + margin: 0px 10px 10px; + } + + .ms-header-right { + float: right; + /*width: 320px;*/ + margin-bottom: 10px; + margin-top: 10px; + margin-right: 20px; + } + + .report-alt-ico { + font-size: 17px; + top: auto; + } + + .report-alt-ico:hover { + color: black; + cursor: pointer; + font-size: 18px; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue b/frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue new file mode 100644 index 0000000000..737f2d643f --- /dev/null +++ b/frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue @@ -0,0 +1,86 @@ +<template> + <div v-if="loadIsOver" v-loading="loading"> + <el-card class="historyCard" v-for="item in reportData" :key="item.id"> + <div slot="header"> + <li style="color:var(--count_number); font-size: 18px"> + <el-input v-if="item.isEdit === 'edit'" size="mini" @blur="updateReport(item)" v-model="item.name"/> + <el-link v-if="item.isEdit !== 'edit'" type="info" @click="selectReport(item.id)" target="_blank" style="color:#303133; font-size: 14px"> + {{ item.name }} + </el-link> + <el-button v-if="showOptionsButton && item.isEdit !== 'edit'" style="float: right; padding: 3px 0; border: 0px;margin-left: 5px" icon="el-icon-delete" size="mini" @click="deleteReport(item.id)"></el-button> + <el-button v-if="showOptionsButton && item.isEdit !== 'edit'" style="float: right; padding: 3px 0; border: 0px" icon="el-icon-edit" size="mini" @click="renameReport(item)"></el-button> + </li> + </div> + <div class="text item"> + <span>{{ item.createTime | timestampFormatDate }}</span> + </div> + </el-card> + </div> +</template> +<script> + +export default { + name: "HistoryReportDataCard", + components: {}, + data() { + return { + loadIsOver: true, + loading: false, + } + }, + props:{ + reportData:Array, + showOptionsButton:Boolean + }, + watch:{ + reportData:{ + handler:function (){ + this.loading = false; + }, + deep:true + } + }, + methods: { + reload(){ + this.loadIsOver = false; + this.$nextTick(() => { + this.loadIsOver = true; + }) + }, + deleteReport(id){ + this.loading = true; + this.$emit("deleteReport",id); + }, + renameReport(item){ + item.isEdit = 'edit'; + this.reload(); + }, + selectReport(id){ + this.$emit("selectReport",id); + }, + updateReport(item){ + let obj = { + name: item.name, + id: item.id + }; + this.$post("/history/report/updateReport", obj, response => { + }); + item.isEdit = ""; + this.reload(); + } + }, +} +</script> + +<style scoped> + +.historyCard{ + border: 0px; +} +/deep/ .el-card__header{ + border: 0px; + padding-bottom: 0px; + padding-top: 5px; +} + +</style> diff --git a/frontend/src/business/components/reportstatistics/router.js b/frontend/src/business/components/reportstatistics/router.js new file mode 100644 index 0000000000..825771c740 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/router.js @@ -0,0 +1,16 @@ +const reportForm = () => import('./ReportAnalysis'); + +export default { + path: "/report", + name: "report", + redirect: "/report/home", + components: { + content: reportForm + }, + children: [ + { + path: 'home', + name: 'reportHome', + }, + ] +} diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue b/frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue new file mode 100644 index 0000000000..fb5c3cf686 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue @@ -0,0 +1,217 @@ +<template> + <div> + + <el-container v-loading="loading" id="reportAnalysis" style="overflow: scroll"> + <el-container class="ms-row"> + <el-aside v-if="!isHide" :width="!isHide ?'235px':'0px'" :style="{ 'max-height': h-50 + 'px', 'margin-left': '5px'}" > + <history-report-data report-type="TEST_CASE_COUNT" + @selectReport="selectReport" @removeHistoryReportId="removeHistoryReportId" + ref="historyReport"/> + </el-aside> + <el-main class="ms-main" style="padding: 0px 5px 0px"> + <div> + <test-case-count-chart @hidePage="hidePage" @orderCharts="orderCharts" ref="analysisChart" + :chart-width="chartWidth" :load-option="loadOption" :pie-option="pieOption"/> + </div> + <div class="ms-row" v-if="!isHide"> + <test-case-count-table :group-name="getGroupNameStr(options.xaxis)" :show-coloums="options.yaxis" :tableData="tableData"/> + </div> + </el-main> + <el-aside v-if="!isHide" style="height: 100%" :width="!isHide ?'485px':'0px'"> + <test-case-count-filter @filterCharts="filterCharts" ref="countFilter"/> + </el-aside> + </el-container> + </el-container> + </div> +</template> + +<script> +import TestCaseCountChart from "./chart/TestCaseCountChart"; +import TestCaseCountTable from "@/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable"; +import TestCaseCountFilter from "./filter/TestCaseCountFilter"; +import {exportPdf,getCurrentProjectID} from "@/common/js/utils"; +import html2canvas from 'html2canvas'; +import HistoryReportData from "../base/HistoryReportData"; + +export default { + name: "TestCaseCountContainer", + components: {TestCaseCountChart, TestCaseCountTable, TestCaseCountFilter, HistoryReportData}, + data() { + return { + isHide: false, + loading: false, + options: {}, + chartWidth: 0, + tableHeight: 300, + loadOption: { + legend: {}, + xAxis: {}, + yAxis: {}, + label: {}, + tooltip: {}, + series: [] + }, + pieOption: { + legend: {}, + label: {}, + tooltip: {}, + series: [], + title: [], + }, + + tableData: [], + h: document.documentElement.clientHeight - 40, + }; + }, + methods: { + handleExport() { + let name = this.$t('commons.report_statistics.test_case_analysis'); + this.$nextTick(function () { + setTimeout(() => { + html2canvas(document.getElementById('reportAnalysis'), { + scale: 2 + }).then(function (canvas) { + exportPdf(name, [canvas]); + }); + }, 1000); + }); + }, + hidePage(isHide) { + this.isHide = isHide; + }, + close() { + this.$emit('closePage'); + }, + init(opt) { + this.loading = true; + this.options = opt; + this.$post(' /report/test/case/count/getReport', opt, response => { + let data = response.data.barChartDTO; + let pieData = response.data.pieChartDTO; + let selectTableData = response.data.tableDTOs; + this.initPic(data,pieData,selectTableData); + + },error => { + this.loading = false; + }); + }, + initPic(barData,pieData,selectTableData){ + this.loading = true; + if (barData) { + this.loadOption.legend = barData.legend; + this.loadOption.xAxis = barData.xaxis; + this.loadOption.xaxis = barData.xaxis; + this.loadOption.series = barData.series; + this.loadOption.grid = { + bottom: '75px',//距离下边距 + }; + this.loadOption.series.forEach(item => { + item.type = this.$refs.analysisChart.chartType; + }); + } + if (pieData) { + this.pieOption.legend = pieData.legend; + this.pieOption.series = pieData.series; + this.pieOption.title = pieData.title; + this.pieOption.grid = { + bottom: '75px',//距离下边距 + }; + if (pieData.width) { + this.pieOption.width = pieData.width; + this.chartWidth = pieData.width; + } + this.pieOption.series.forEach(item => { + item.type = this.$refs.analysisChart.chartType; + }); + } + if (selectTableData) { + this.tableData = selectTableData; + } + this.loading = false; + this.$refs.analysisChart.reload(); + }, + filterCharts(opt) { + this.init(opt); + }, + orderCharts(order) { + this.options.order = order; + this.filterCharts(this.options); + }, + saveReport() { + let obj = {}; + obj.projectId = getCurrentProjectID(); + obj.selectOption = JSON.stringify(this.options); + let dataOptionObj = { + loadOption: this.loadOption, + pieOption: this.pieOption, + tableData: this.tableData, + }; + obj.dataOption = JSON.stringify(dataOptionObj); + obj.reportType = 'TEST_CASE_COUNT'; + this.$post("/history/report/saveReport", obj, response => { + this.$alert(this.$t('commons.save_success')); + this.$refs.historyReport.initReportData(); + }); + }, + selectReport(selectId){ + if(selectId){ + this.loading = true; + let paramObj = { + id:selectId + } + this.$post('/history/report/selectById',paramObj, response => { + let reportData = response.data; + if(reportData){ + if(reportData.dataOption){ + let dataOptionObj = JSON.parse(reportData.dataOption); + this.initPic(dataOptionObj.loadOption,dataOptionObj.pieOption,dataOptionObj.tableData); + } + if(reportData.selectOption){ + let selectOptionObj = JSON.parse(reportData.selectOption); + this.$refs.countFilter.initSelectOption(selectOptionObj); + } + + this.loading = false; + } + }, (error) => { + this.loading = false; + }); + this.$emit('initHistoryReportId',selectId); + } + }, + removeHistoryReportId(){ + this.$emit('initHistoryReportId',""); + }, + getGroupNameStr(groupName){ + if(groupName === 'creator') { + return this.$t('commons.report_statistics.report_filter.select_options.creator'); + }else if(groupName === 'maintainer'){ + return this.$t('commons.report_statistics.report_filter.select_options.maintainer'); + }else if(groupName === 'casetype'){ + return this.$t('commons.report_statistics.report_filter.select_options.case_type'); + }else if(groupName === 'casestatus'){ + return this.$t('commons.report_statistics.report_filter.select_options.case_status'); + }else if(groupName === 'caselevel'){ + return this.$t('commons.report_statistics.report_filter.select_options.case_level'); + }else { + return ""; + } + }, + selectAndSaveReport(){ + let opt = this.$refs.countFilter.getOption(); + this.options = opt; + this.saveReport(); + } + }, +}; +</script> + +<style scoped> +.ms-row { + padding-top: 5px; +} + +/deep/ .el-main { + padding: 0px 20px 0px; +} +</style> diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue b/frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue new file mode 100644 index 0000000000..5ada182cfa --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue @@ -0,0 +1,284 @@ +<template> + <div v-loading="loading"> + <el-card class="ms-test-chart" :style="{ width: w+'px', height: h + 'px'}" ref="msDrawer"> + <el-row class="ms-row"> + <p class="tip"><span style="margin-left: 5px"></span> {{$t('commons.report_statistics.chart')}} </p> + <div class="ms-test-chart-header"> + <el-dropdown @command="exportCommand" :hide-on-click="false"> + <span class="el-dropdown-link"> + {{ $t('commons.export') }}<i class="el-icon-arrow-down el-icon--right"></i> + </span> + <el-dropdown-menu slot="dropdown"> + <el-dropdown-item command="jpg">JPG</el-dropdown-item> + <el-dropdown-item command="png">PNG</el-dropdown-item> + </el-dropdown-menu> + </el-dropdown> + <span style="margin: 0px 10px 10px">|</span> + <el-select v-model="chartType" class="ms-col-type" size="mini" style="width: 100px" @change="generateOption"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in charts"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <el-select v-model="order" class="ms-col-type" size="mini" style="width: 120px" @change="orderCharts"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in orders"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <font-awesome-icon v-if="!isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'expand-alt']" size="lg" @click="fullScreen"/> + <font-awesome-icon v-if="isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'compress-alt']" size="lg" @click="unFullScreen"/> + </div> + </el-row> + <el-row style="overflow: auto"> + <ms-chart ref="chart1" v-if="!loading" :options="dataOption" :style="{width: chartWidthNumber+'px', height: (h-50) + 'px'}" class="chart-config" :autoresize="true" id="picChart"/> + </el-row> + </el-card> + </div> +</template> + +<script> + import echarts from "echarts"; + import MsChart from "@/business/components/common/chart/MsChart"; + + export default { + name: "TestCaseCountChart", + components: {MsChart}, + props: { + loadOption: {}, + pieOption: {}, + chartWidth:Number, + }, + data() { + return { + dataOption:{}, + x: 0, + y: 0, + w: document.documentElement.clientWidth - 760, + h: document.documentElement.clientHeight * 0.5 , + chartWidthNumber:document.documentElement.clientWidth - 760, + isFullScreen: false, + originalW: 100, + originalH: 100, + showFullScreen: { + type: Boolean, + default() { + return true; + } + }, + // 头部部分 + chartType: "bar", + charts: [ + {id: 'bar', name: this.$t('commons.report_statistics.bar')}, + {id: 'pie', name: this.$t('commons.report_statistics.pie')} + ], + order: "", + orders: [{id: '', name: '默认排序'},{id: 'desc', name: this.$t('commons.report_statistics.desc')}, {id: 'asc', name: this.$t('commons.report_statistics.asc')}], + loading: false, + options: {}, + pieItemOption:{ + dataset: [{ + source: [ + ['Product', 'Sales', 'Price', 'Year'], + ['Cake', 123, 32, 2011], + ['Cereal', 231, 14, 2011], + ['Tofu', 235, 5, 2011], + ['Dumpling', 341, 25, 2011], + ['Biscuit', 122, 29, 2011], + ['Cake', 143, 30, 2012], + ['Cereal', 201, 19, 2012], + ['Tofu', 255, 7, 2012], + ['Dumpling', 241, 27, 2012], + ['Biscuit', 102, 34, 2012], + ['Cake', 153, 28, 2013], + ['Cereal', 181, 21, 2013], + ['Tofu', 395, 4, 2013], + ['Dumpling', 281, 31, 2013], + ['Biscuit', 92, 39, 2013], + ['Cake', 223, 29, 2014], + ['Cereal', 211, 17, 2014], + ['Tofu', 345, 3, 2014], + ['Dumpling', 211, 35, 2014], + ['Biscuit', 72, 24, 2014], + ], + }, { + transform: { + type: 'filter', + config: { dimension: 'Year', value: 2011 } + }, + }, { + transform: { + type: 'filter', + config: { dimension: 'Year', value: 2012 } + } + }, { + transform: { + type: 'filter', + config: { dimension: 'Year', value: 2013 } + } + }], + series: [{ + type: 'pie', radius: 50, center: ['50%', '25%'], + datasetIndex: 1 + }, { + type: 'pie', radius: 50, center: ['50%', '50%'], + datasetIndex: 2 + }, { + type: 'pie', radius: 50, center: ['50%', '75%'], + datasetIndex: 3 + }], + media: [{ + query: { minAspectRatio: 1 }, + option: { + series: [ + { center: ['25%', '50%'] }, + { center: ['50%', '50%'] }, + { center: ['75%', '50%'] } + ] + } + }, { + option: { + series: [ + { center: ['50%', '25%'] }, + { center: ['50%', '50%'] }, + { center: ['50%', '75%'] } + ] + } + }] + }, + } + }, + created() { + this.dataOption = this.loadOption; + }, + watch:{ + chartWidth(){ + this.countChartWidth(); + }, + chartType(){ + this.countChartWidth(); + } + }, + methods: { + countChartWidth(){ + if(this.chartWidth === 0 || this.chartType === 'bar'){ + this.chartWidthNumber = this.w; + }else { + this.chartWidthNumber = this.chartWidth; + } + }, + orderCharts() { + this.$emit('orderCharts', this.order); + }, + generateOption() { + if(this.chartType === 'pie'){ + this.dataOption = this.pieOption; + }else { + this.dataOption = this.loadOption; + } + this.dataOption.series.forEach(item => { + item.type = this.chartType; + }) + this.reload(); + }, + reload() { + this.loading = true + this.$nextTick(() => { + this.loading = false + }) + }, + fullScreen() { + this.originalW = this.w; + this.originalH = this.h; + this.w = document.body.clientWidth - 50; + this.h = document.body.clientHeight; + this.isFullScreen = true; + this.$emit('hidePage', true); + }, + unFullScreen() { + this.w = this.originalW; + this.h = this.originalH; + this.isFullScreen = false; + this.$emit('hidePage', false); + }, + exportCommand(command){ + let fileName = 'report_pic.'+command; + if (document.getElementById('picChart')) { + let chartsCanvas = document.getElementById('picChart').querySelectorAll('canvas')[0] + let mime = 'image/png'; + if(command === 'jpg'){ + mime = 'image/jpg'; + } + if (chartsCanvas) { + // toDataURL()是canvas对象的一种方法,用于将canvas对象转换为base64位编码 + let imageUrl = chartsCanvas && chartsCanvas.toDataURL(mime) + if (navigator.userAgent.indexOf('Trident') > -1) { + // IE11 + let arr = imageUrl.split(',') + // atob() 函数对已经使用base64编码编码的数据字符串进行解码 + let bstr = atob(arr[1]) + let bstrLen = bstr.length + // Uint8Array, 开辟 8 位无符号整数值的类型化数组。内容将初始化为 0 + let u8arr = new Uint8Array(bstrLen) + while (bstrLen--) { + // charCodeAt() 方法可返回指定位置的字符的 Unicode 编码 + u8arr[bstrLen] = bstr.charCodeAt(bstrLen) + } + // msSaveOrOpenBlob 方法允许用户在客户端上保存文件,方法如同从 Internet 下载文件,这是此类文件保存到“下载”文件夹的原因 + window.navigator.msSaveOrOpenBlob(new Blob([u8arr], {type: mime}), fileName ); + } else { + // 其他浏览器 + let $a = document.createElement('a') + $a.setAttribute('href', imageUrl) + $a.setAttribute('download', fileName) + $a.click() + } + } + } + }, + }, + } +</script> + +<style scoped> + + .ms-test-chart-header { + z-index: 999; + width: 380px; + float: right; + margin-right: 10px; + } + + .report-alt-ico { + font-size: 15px; + margin: 0px 10px 0px; + color: #8c939d; + } + + .report-alt-ico:hover { + color: black; + cursor: pointer; + font-size: 18px; + } + + /deep/ .echarts { + height: calc(100vh / 1.95); + } + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-row { + padding-top: 10px; + } + + .chart-config { + width: 100%; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue b/frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue new file mode 100644 index 0000000000..cf43fdbce6 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue @@ -0,0 +1,401 @@ +<template> + <div v-loading="loading"> + <el-card :style="{height: h + 'px'}" class="ms-card"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span> {{ $t('commons.report_statistics.options') }}</p> + </el-row> + <el-row class="ms-row"> + <p>{{ $t('commons.report_statistics.report_filter.xaxis') }}</p> + <el-select class="ms-http-select" size="small" v-model="option.xaxis" style="width: 100%"> + <el-option v-for="item in xAxisOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </el-row> + <el-row class="ms-row"> + <p>{{ $t('commons.report_statistics.report_filter.yaxis') }}</p> + <el-select class="ms-http-select" size="small" v-model="option.yaxis" multiple style="width: 100%"> + <el-option v-for="item in yAxisOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </el-row> + <el-row class="ms-row"> + <p>{{ $t('commons.create_time')}}</p> + <div style="width: 25%;float: left"> + <el-select class="ms-http-select" size="small" v-model="option.timeType" > + <el-option v-for="item in timeTypeOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </div> + <div v-if="option.timeType === 'fixedTime'" style="width: 70%;margin-left: 20px;float: left"> + <el-date-picker + size="small" + v-model="option.times" + type="datetimerange" + value-format="timestamp" + :range-separator="$t('api_monitor.to')" + :start-placeholder="$t('commons.date.start_date')" + :end-placeholder="$t('commons.date.end_date')" + :picker-options="datePickerOptions" + style="margin-left: 10px;width: 100%"> + </el-date-picker> + </div> + <div v-if="option.timeType === 'dynamicTime'" style="width: 70%;margin-left: 20px;float: left"> + <span style="width: 20%">{{ $t('commons.report_statistics.report_filter.recently') }}</span> + <el-select class="ms-http-select" size="small" v-model="option.timeFilter.timeRange" style="width: 30%;margin-left: 10px;width: 40%"> + <el-option v-for="item in timeRangeNumberMax" :key="item" :label="item" :value="item"/> + </el-select> + <el-select class="ms-http-select" size="small" v-model="option.timeFilter.timeRangeUnit" + @change="timeRangeUnitChange" + style="width: 30%;margin-left: 10px;width: 40%"> + <el-option v-for="item in timeRangeUnitOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </div> + </el-row> + <el-row class="ms-row" style="margin-left: 0px;margin-right: 0px; margin-top: 20px"> + <el-collapse v-model="collapseActiveNames"> + <el-collapse-item :title="$t('commons.report_statistics.report_filter.more_options')" name="1"> + <el-container> + <el-aside width="73px" style="overflow: hidden"> + <div v-if="option.filters.length > 1" style="height: 100%" id="moreOptionTypeDiv"> + <div class="top-line-box" :style="{ height:lineDivHeight+'px'}"> + </div> + <div> + <el-select class="ms-http-select" size="small" v-model="option.filterType" style="width: 70px"> + <el-option v-for="item in filterTypes" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </div> + <div class="bottom-line-box" :style="{ height:lineDivHeight+'px'}"> + </div> + </div> + </el-aside> + <el-main v-if="optionLoad" style="padding: 0px"> + <el-row v-for="filterItem in option.filters" :key="filterItem.id" style="margin-bottom: 5px"> + <el-col :span="24"> + <el-select style="width: 100px" class="ms-http-select" size="small" v-model="filterItem.type"> + <el-option v-for="item in getFilterOptionKey(filterItem.type)" :key="item.type" :label="item.name" :value="item.type"/> + </el-select> + <span style="margin-left:10px;margin-right:10px">{{ $t('commons.report_statistics.report_filter.belone') }}</span> + + <el-select style="width:173px" :collapse-tags="true" class="ms-http-select" size="small" multiple filterable v-model="filterItem.values" v-if="getFilterOptions(filterItem.type).length > 0"> + <el-option v-for="itemOption in getFilterOptions(filterItem.type)" :key="itemOption.id" :label="itemOption.label" :value="itemOption.id"/> + </el-select> + <el-input style="width:173px" v-model="filterItem.value" size="small" v-else ></el-input> + <el-button @click="addFilterOptions(filterItem.type)" + @keydown.enter.native.prevent + type="primary" + icon="el-icon-plus" + circle + style="color:white;padding: 0px 0.1px;width: 20px;height: 20px;margin-left:5px;" + size="mini"/> + <el-button @click="removeFilterOptions(filterItem.type)" + @keydown.enter.native.prevent + type="danger" + icon="el-icon-minus" + circle + style="color:white;padding: 0px 0.1px;width: 20px;height: 20px;margin-left:5px;" + size="mini"/> + </el-col> + </el-row> + </el-main> + </el-container> + </el-collapse-item> + </el-collapse> + </el-row> + + + <el-row type="flex"> + <el-col style="height: 100%" :span="4" > + + </el-col> + <el-col :span="20"> + + </el-col> + </el-row> + <el-row align="middle"> + <el-button style="margin-left: 200px;margin-top: 20px" type="primary" size="mini" @click="init">{{ $t('commons.confirm') }}</el-button> + </el-row> + </el-card> + </div> +</template> + +<script> +import {getCurrentProjectID, getUUID} from "@/common/js/utils"; +import MsSelectTree from "@/business/components/common/select-tree/SelectTree"; + +export default { + name: "TestAnalysisTable", + components: {MsSelectTree}, + data() { + return { + collapseActiveNames: "", + option: { + xaxis: "creator", + yaxis: ["testCase","apiCase","scenarioCase","loadCase"], + timeType: "dynamicTime", + projectId: getCurrentProjectID(), + filterType: "And", + timeFilter:{ + timeRange: 7, + timeRangeUnit: "day", + }, + times: [new Date().getTime() - 6 * 24 * 3600 * 1000, new Date().getTime()], + filters:[ + { + type:"", + name:"", + compType:"input", + isShow:false, + }, + ], + }, + h: document.documentElement.clientHeight + 80, + lineDivHeight: 0, + disabled: false, + loading: false, + optionLoad: true, + result: {}, + items: [], + modules: [], + xAxisOptions: [ + {id: 'creator', label: this.$t('commons.report_statistics.report_filter.select_options.creator')}, + {id: 'maintainer', label: this.$t('commons.report_statistics.report_filter.select_options.maintainer')}, + {id: 'casetype', label: this.$t('commons.report_statistics.report_filter.select_options.case_type')}, + {id: 'casestatus', label: this.$t('commons.report_statistics.report_filter.select_options.case_status')}, + {id: 'caselevel', label: this.$t('commons.report_statistics.report_filter.select_options.case_level')}, + ], + yAxisOptions: [ + {id: 'testCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.functional')}, + {id: 'apiCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.api')}, + {id: 'scenarioCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.scene')}, + {id: 'loadCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.load')}, + ], + filterTypes: [ + {id: 'And', label: 'And'}, + {id: 'Or', label: 'Or'}, + ], + timeTypeOptions: [ + {id: 'fixedTime', label: this.$t('commons.report_statistics.report_filter.time_options.fixed_time')}, + {id: 'dynamicTime', label: this.$t('commons.report_statistics.report_filter.time_options.dynamic_time')}, + ], + timeRangeNumberMax: 31, + timeRangeUnitOptions: [ + {id: 'day', label: this.$t('commons.report_statistics.report_filter.time_options.day')}, + {id: 'month', label: this.$t('commons.report_statistics.report_filter.time_options.month')}, + {id: 'year', label: this.$t('commons.report_statistics.report_filter.time_options.year')}, + ], + priorityFilters: [ + {id: 'P0', label: 'P0'}, + {id: 'P1', label: 'P1'}, + {id: 'P2', label: 'P2'}, + {id: 'P3', label: 'P3'} + ], + moduleObj: { + id: 'id', + label: 'name', + }, + moreOptionsSelectorKeys:[ + { + type:"casetype", + name:this.$t('commons.report_statistics.report_filter.select_options.case_type'), + }, + { + type:"creator", + name:this.$t('commons.report_statistics.report_filter.select_options.creator'), + }, + { + type:"maintainer", + name:this.$t('commons.report_statistics.report_filter.select_options.maintainer'), + }, + { + type:"casestatus", + name:this.$t('commons.report_statistics.report_filter.select_options.case_status'), + }, + { + type:"caselevel", + name:this.$t('commons.report_statistics.report_filter.select_options.case_level'), + }, + ], + moreOptionsSelectorValues: { + id: 'id', + label: 'label', + }, + datePickerOptions: { + disabledDate: (time) => { + let nowDate = new Date(); + let oneDay = 1000 * 60 * 60 * 24; + let oneYearLater = new Date(nowDate.getTime() + (oneDay * 365)); + return time.getTime() > nowDate || time.getTime() > oneYearLater;//注意是||不是&& + } + }, + }; + }, + created() { + this.init(); + this.initMoreOptionsSelectorValues(); + }, + computed: { + }, + watch: { + option: { + handler: function () { + this.$nextTick(() => { + this.lineDivHeight = 0; + // let elem = document.getElementById("moreOptionTypeDiv"); + if(this.option.filters.length > 1){ + let countPageHeight = (this.option.filters.length)* 32 + (this.option.filters.length-1)*5; + if(countPageHeight > 32){ + this.lineDivHeight = (countPageHeight-32)/2-11; + } + } + }); + }, + deep: true + } + }, + methods: { + initSelectOption(opt){ + this.loading = true; + this.option = opt; + this.$nextTick(() => { + this.loading = false; + }); + }, + addFilterOptions: function (type){ + this.optionLoad = false; + let otherOptionKeys = this.getFilterOptionKey(""); + if(otherOptionKeys.length > 0 && this.option.filters.length < 5) { + let addOptions = { + type: "", + id: getUUID(), + name: "", + compType: "selector", + isShow: false, + itemOptions: this.priorityFilters, + }; + this.option.filters.push(addOptions); + } else { + this.$alert(this.$t('commons.report_statistics.alert.cannot_add_more_options')); + } + this.$nextTick(() => { + this.optionLoad = true; + }); + }, + getFilterOptions(type){ + let optionArray = []; + if(this.moreOptionsSelectorValues && this.moreOptionsSelectorValues[type]){ + optionArray = this.moreOptionsSelectorValues[type]; + } + return optionArray; + }, + getFilterOptionKey(type){ + let optionArray = []; + for(let i = 0; i < this.moreOptionsSelectorKeys.length; i++){ + let keyObj = this.moreOptionsSelectorKeys[i]; + let inOptions = false; + if(keyObj.type !== type){ + for(let j = 0; j < this.option.filters.length; j ++){ + if(keyObj.type === this.option.filters[j].type){ + inOptions = true; + } + } + } + if(!inOptions){ + optionArray.push(keyObj); + } + } + return optionArray; + }, + removeFilterOptions: function (type){ + let removeOptionsIndex = -1; + for(let index = 0; index < this.option.filters.length; index ++){ + let item = this.option.filters[index]; + if(item.type === type){ + removeOptionsIndex = index; + } + } + if(removeOptionsIndex >= 0){ + this.option.filters.splice(removeOptionsIndex,1); + } + if(this.option.filters.length === 0){ + let addOptions = { + type: "", + id: getUUID(), + name: "", + compType: "selector", + isShow: false, + itemOptions: this.priorityFilters, + }; + this.option.filters.push(addOptions); + } + }, + init: function () { + this.$emit('filterCharts', this.option); + }, + onTimeChange() { + if (this.option.times[1] > new Date().getTime()) { + this.$alert(this.$t('commons.report_statistics.alert.end_time_cannot_over_than_start_time')); + } + }, + initMoreOptionsSelectorValues() { + let selectParam = { + projectId:getCurrentProjectID() + }; + this.$post('/report/test/case/count/initDatas',selectParam, response => { + this.moreOptionsSelectorValues = response.data; + }); + }, + timeRangeUnitChange(val){ + if(val === 'day'){ + this.timeRangeNumberMax = 31; + }else if(val === 'month'){ + this.timeRangeNumberMax = 12; + }else { + this.timeRangeNumberMax = 1; + } + this.option.timeFilter.timeRange = 1; + }, + getOption(){ + return this.option; + } + }, +}; +</script> + +<style scoped> + +.tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 10px 0px; +} + +.ms-row { + margin: 0px 10px 0px; +} + +.ms-card { + width: 480px; +} + +.top-line-box{ + border-top: 1px solid; + border-left: 1px solid; + margin-left: 32px; + margin-top: 10px; + border-top-left-radius: 10px; +} + +.bottom-line-box{ + border-bottom: 1px solid; + border-left: 1px solid; + margin-left: 32px; + border-bottom-left-radius: 10px; +} + +/deep/ .el-select__tags-text { + display: inline-block; + max-width: 50px; + overflow: hidden; + text-overflow:ellipsis; + white-space: nowrap; +} +</style> diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue b/frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue new file mode 100644 index 0000000000..833ec2da74 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue @@ -0,0 +1,113 @@ +<template> + <div v-loading="loading" class="ms-div"> + <el-card :style="{ width: w+'px'}"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span>{{$t('commons.report_statistics.excel')}} </p> + </el-row> + <el-row> + <el-table + :data="tableData" + :max-height="tableHeight" + :tree-props="{children: 'children', hasChildren: 'hasChildren'}" + row-key="id" + border + class="ms-table"> + <el-table-column + prop="name" + :label="groupName"> + </el-table-column> + <el-table-column + prop="allCount" + label="总计"> + </el-table-column> + <el-table-column + prop="testCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.functional')" + v-if="isShowColumn('testCase')" + > + </el-table-column> + <el-table-column + prop="apiCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.api')" + v-if="isShowColumn('apiCase')" + > + </el-table-column> + <el-table-column + prop="scenarioCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.scene')" + v-if="isShowColumn('scenarioCase')" + > + </el-table-column> + <el-table-column + prop="loadCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.load')" + v-if="isShowColumn('loadCase')" + > + </el-table-column> + </el-table> + </el-row> + </el-card> + </div> +</template> + +<script> + export default { + name: "TestAnalysisTable", + components: {}, + props: { + tableData: Array, + groupName: String, + showColoums: Array, + }, + data() { + return { + tableHeight : "100px", + w: document.documentElement.clientWidth - 760, + loading: false, + } + }, + created() { + this.getTableHeight(); + }, + methods: { + isShowColumn(type){ + if(this.showColoums){ + return this.showColoums.findIndex(item => item=== type) >= 0; + }else { + return false; + } + + }, + getTableHeight(){ + let countNumber = document.documentElement.clientHeight * 0.4 /1 - 140; + countNumber = Math.ceil(countNumber); + this.tableHeight = countNumber + 'px'; + } + }, + } +</script> + +<style scoped> + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-div { + margin-bottom: 20px; + } + + .ms-table { + width: 95%; + margin: 20px; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue b/frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue new file mode 100644 index 0000000000..a68a6ae9ee --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue @@ -0,0 +1,168 @@ +<template> + <div :style="{ height: h + 'px'}"> + <el-container v-loading="loading" id="reportAnalysis" style="overflow: scroll"> + <el-container class="ms-row"> + <el-aside :width="!isHide ?'235px':'0px'" style="margin-left: 5px; max-height: 843px"> + <history-report-data report-type="TEST_CASE_ANALYSIS" + @selectReport="selectReport" @removeHistoryReportId="removeHistoryReportId" + ref="historyReport"/> + </el-aside> + <el-main class="ms-main"> + <div> + <test-analysis-chart @hidePage="hidePage" @orderCharts="orderCharts" ref="analysisChart" :load-option="loadOption"/> + </div> + <div class="ms-row" v-if="!isHide"> + <test-analysis-table :tableData="tableData"/> + </div> + </el-main> + <el-aside :width="!isHide ?'485px':'0px'"> + <test-analysis-filter @filterCharts="filterCharts" ref="analysisFilter"/> + </el-aside> + </el-container> + </el-container> + </div> +</template> + +<script> + import TestAnalysisChart from "./chart/TestAnalysisChart"; + import TestAnalysisTable from "./table/TestAnalysisTable"; + import TestAnalysisFilter from "./filter/TestAnalysisFilter"; + import {exportPdf, getCurrentProjectID} from "@/common/js/utils"; + import html2canvas from 'html2canvas'; + import HistoryReportData from "../base/HistoryReportData"; + + export default { + name: "TestAnalysisContainer", + components: {TestAnalysisChart, TestAnalysisTable, TestAnalysisFilter, HistoryReportData}, + data() { + return { + isHide: false, + loading: false, + options: {}, + loadOption: { + legend: {}, + xAxis: {}, + yAxis: {}, + label: {}, + tooltip: {}, + series: [] + }, + tableData: [], + h: document.documentElement.clientHeight - 40, + } + }, + methods: { + handleExport() { + let name = this.$t('commons.report_statistics.test_case_analysis'); + this.$nextTick(function () { + setTimeout(() => { + html2canvas(document.getElementById('reportAnalysis'), { + scale: 2 + }).then(function (canvas) { + exportPdf(name, [canvas]); + }); + }, 1000); + }); + }, + hidePage(isHide) { + this.isHide = isHide; + }, + close() { + this.$emit('closePage'); + }, + init(opt) { + this.loading = true; + this.options = opt; + this.$post(' /report/test/analysis/getReport', opt, response => { + let data = response.data.chartDTO; + let tableDTOs = response.data.tableDTOs; + this.initPic(data,tableDTOs); + }); + }, + filterCharts(opt) { + this.init(opt); + }, + orderCharts(order) { + this.options.order = order; + this.filterCharts(this.options); + }, + saveReport() { + let obj = {}; + obj.projectId = getCurrentProjectID(); + obj.selectOption = JSON.stringify(this.options); + let dataOptionObj = { + loadOption: this.loadOption, + pieOption: this.pieOption, + tableData: this.tableData, + }; + obj.dataOption = JSON.stringify(dataOptionObj); + obj.reportType = 'TEST_CASE_ANALYSIS'; + this.$post("/history/report/saveReport", obj, response => { + this.$alert(this.$t('commons.save_success')); + this.$refs.historyReport.initReportData(); + }); + }, + initPic(loadOption,tableData){ + this.loading = true; + if (loadOption) { + this.loadOption.legend = loadOption.legend; + this.loadOption.xAxis = loadOption.xAxis; + this.loadOption.series = loadOption.series; + this.loadOption.grid = { + bottom: '75px',//距离下边距 + } + this.loadOption.series.forEach(item => { + item.type = this.$refs.analysisChart.chartType; + }) + } + if (tableData) { + this.tableData = tableData; + } + this.loading = false; + }, + selectReport(selectId){ + if(selectId){ + this.loading = true; + let paramObj = { + id:selectId + } + this.$post('/history/report/selectById',paramObj, response => { + let reportData = response.data; + if(reportData){ + if(reportData.dataOption){ + let dataOptionObj = JSON.parse(reportData.dataOption); + this.initPic(dataOptionObj.loadOption,dataOptionObj.pieOption,dataOptionObj.tableData); + } + if(reportData.selectOption){ + let selectOptionObj = JSON.parse(reportData.selectOption); + this.$refs.analysisFilter.initSelectOption(selectOptionObj); + } + } + this.loading = false; + }, (error) => { + this.loading = false; + }); + this.$emit('initHistoryReportId',selectId); + } + }, + removeHistoryReportId(){ + this.$emit('initHistoryReportId',""); + }, + selectAndSaveReport(){ + let opt = this.$refs.analysisFilter.getOption(); + this.options = opt; + this.saveReport(); + } + }, + } +</script> + +<style scoped> + .ms-row { + padding-top: 10px; + } + + /deep/ .el-main { + padding: 0px 20px 0px; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue b/frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue new file mode 100644 index 0000000000..cc88b0abc2 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue @@ -0,0 +1,143 @@ +<template> + <div v-loading="loading"> + <el-card class="ms-test-chart" :style="{ width: w+'px', height: h + 'px'}" ref="msDrawer"> + <el-row class="ms-row"> + <p class="tip"><span style="margin-left: 5px"></span> {{$t('commons.report_statistics.chart')}} </p> + <div class="ms-test-chart-header"> + <el-select v-model="chartType" class="ms-col-type" size="mini" style="width: 100px" @change="generateOption"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in charts"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <el-select v-model="order" class="ms-col-type" size="mini" style="width: 120px" @change="orderCharts"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in orders"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <font-awesome-icon v-if="!isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'expand-alt']" size="lg" @click="fullScreen"/> + <font-awesome-icon v-if="isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'compress-alt']" size="lg" @click="unFullScreen"/> + </div> + </el-row> + <el-row> + <ms-chart ref="chart1" :options="loadOption" class="chart-config" :autoresize="true"/> + </el-row> + </el-card> + </div> +</template> + +<script> + // 这个引用不能删除,删除后图例不显示 + import echarts from "echarts"; + import MsChart from "@/business/components/common/chart/MsChart"; + + export default { + name: "TestAnalysisChart", + components: {MsChart}, + props: { + loadOption: {}, + }, + data() { + return { + x: 0, + y: 0, + w: document.documentElement.clientWidth - 760, + h: document.documentElement.clientHeight / 1.7, + isFullScreen: false, + originalW: 100, + originalH: 100, + showFullScreen: { + type: Boolean, + default() { + return true; + } + }, + // 头部部分 + chartType: "line", + charts: [{id: 'line', name: this.$t('commons.report_statistics.line')}, {id: 'bar', name: this.$t('commons.report_statistics.bar')}], + order: "", + orders: [{id: '', name: '默认排序'},{id: 'desc', name: this.$t('commons.report_statistics.desc')}, {id: 'asc', name: this.$t('commons.report_statistics.asc')}], + loading: false, + options: {}, + } + }, + methods: { + orderCharts() { + this.$emit('orderCharts', this.order); + }, + generateOption() { + this.loadOption.series.forEach(item => { + item.type = this.chartType; + }) + this.reload(); + }, + reload() { + this.loading = true + this.$nextTick(() => { + this.loading = false + }) + }, + fullScreen() { + this.originalW = this.w; + this.originalH = this.h; + this.w = document.body.clientWidth - 50; + this.h = document.body.clientHeight; + this.isFullScreen = true; + this.$emit('hidePage', true); + }, + unFullScreen() { + this.w = this.originalW; + this.h = this.originalH; + this.isFullScreen = false; + this.$emit('hidePage', false); + }, + getOptions(){ + return this.loadOption; + } + }, + } +</script> + +<style scoped> + + .ms-test-chart-header { + z-index: 999; + width: 320px; + float: right; + margin-right: 10px; + } + + .report-alt-ico { + font-size: 15px; + margin: 0px 10px 0px; + color: #8c939d; + } + + .report-alt-ico:hover { + color: black; + cursor: pointer; + font-size: 18px; + } + + /deep/ .echarts { + height: calc(100vh / 1.95); + } + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-row { + padding-top: 10px; + } + + .chart-config { + width: 100%; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue b/frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue new file mode 100644 index 0000000000..5cb3e70a8e --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue @@ -0,0 +1,225 @@ +<template> + <div v-loading="loading"> + <el-card :style="{height: h + 'px'}" class="ms-card"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span> {{$t('commons.report_statistics.options')}}</p> + </el-row> + <el-row class="ms-row"> + <p>{{$t('commons.report_statistics.type')}}</p> + <el-checkbox v-model="option.createCase">{{$t('commons.report_statistics.add_case')}}</el-checkbox> + <el-checkbox v-model="option.updateCase">{{$t('commons.report_statistics.change_case')}}</el-checkbox> + </el-row> + <el-row class="ms-row"> + <p> {{$t('api_monitor.date')}}</p> + <el-date-picker + size="small" + v-model="option.times" + type="datetimerange" + value-format="timestamp" + range-separator="至" + start-placeholder="开始日期" + end-placeholder="结束日期" + :picker-options="datePickerOptions" + style="width: 100%"> + </el-date-picker> + </el-row> + <el-row class="ms-row"> + <p>{{$t('commons.project')}}</p> + <ms-select-tree size="small" :data="items" :default-key="projectDefaultKey" @getValue="setProjects" :obj="obj" clearable checkStrictly multiple ref="projectSelector"/> + </el-row> + <el-row class="ms-row"> + <p>{{$t('test_track.module.module')}}</p> + <ms-select-tree size="small" :data="modules" :default-key="moduleDefaultKey" :disabled="disabled" @getValue="setModules" :obj="moduleObj" clearable checkStrictly multiple ref="moduleSelector"/> + </el-row> + <el-row class="ms-row"> + <p>{{$t('api_test.automation.case_level')}}</p> + <el-select class="ms-http-select" size="small" v-model="option.prioritys" multiple style="width: 100%"> + <el-option v-for="item in priorityFilters" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </el-row> + <el-row class="ms-row"> + <p>{{$t('test_track.case.maintainer')}}</p> + <ms-select-tree size="small" :data="maintainerOptions" :default-key="userDefaultKey" @getValue="setUsers" :obj="moduleObj" clearable checkStrictly multiple ref="userSelector"/> + </el-row> + + </el-card> + </div> +</template> + +<script> + import {getCurrentProjectID} from "@/common/js/utils"; + import MsSelectTree from "@/business/components/common/select-tree/SelectTree"; + + export default { + name: "TestAnalysisTable", + components: {MsSelectTree}, + data() { + return { + option: {createCase: true, updateCase: true, projects: [], times: [new Date().getTime() - 6 * 24 * 3600 * 1000, new Date().getTime()]}, + h: document.documentElement.clientHeight + 80, + disabled: false, + loading: false, + result: {}, + items: [], + projectDefaultKey:[], + moduleDefaultKey:[], + userDefaultKey:[], + modules: [], + maintainerOptions: [], + priorityFilters: [ + {id: 'P0', label: 'P0'}, + {id: 'P1', label: 'P1'}, + {id: 'P2', label: 'P2'}, + {id: 'P3', label: 'P3'} + ], + syncReport: true, + moduleObj: { + id: 'id', + label: 'name', + }, + obj: { + id: 'id', + label: 'label', + }, + datePickerOptions: { + disabledDate: (time) => { + let nowDate = new Date(); + let oneDay = 1000 * 60 * 60 * 24; + let oneYearLater = new Date(nowDate.getTime() + (oneDay * 365)); + return time.getTime() > nowDate || time.getTime() > oneYearLater;//注意是||不是&& + } + }, + } + }, + created() { + this.init(); + this.initUsers(); + }, + watch: { + option: { + handler: function () { + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + deep: true + } + }, + methods: { + initSelectOption(opt){ + if(opt){ + this.syncReport = false; + this.loading = true; + this.option = opt; + if(opt.projects){ + this.projectDefaultKey = opt.projects; + }else { + this.projectDefaultKey = []; + } + if(opt.modules && this.projectDefaultKey.length === 1){ + this.moduleDefaultKey = opt.modules; + }else { + this.moduleDefaultKey = []; + } + if(opt.users){ + this.userDefaultKey = opt.users; + }else { + this.userDefaultKey = []; + } + this.$nextTick(() => { + this.loading = false; + this.syncReport = true; + }); + } + }, + init: function () { + this.result = this.$get("/project/listAll", response => { + let projects = response.data; + if (projects) { + this.items = []; + projects.forEach(item => { + let data = {id: item.id, label: item.name}; + this.items.push(data); + }) + } + }) + }, + onTimeChange() { + if (this.option.times[1] > new Date().getTime()) { + this.$error("结束时间不能超过当前时间"); + } + }, + initModule() { + this.result = this.$get("/case/node/list/" + this.option.projects[0], response => { + this.modules = response.data; + this.$refs.moduleSelector.setKeys(this.moduleDefaultKey); + }) + }, + initUsers() { + this.$post('/user/project/member/tester/list', {projectId: getCurrentProjectID()}, response => { + this.maintainerOptions = response.data; + }); + }, + setProjects(key, data) {//获取子组件值 + if(!key || key === ""){ + key = []; + } + this.option.projects = key; + this.modules = []; + if (key && key.length > 1) { + this.moduleDefaultKey = []; + this.disabled = true; + } else { + this.disabled = false; + } + if (this.option.projects && this.option.projects.length == 1) { + this.initModule(); + } + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + setModules(key, data) {//获取子组件值 + if(!key || key === ""){ + key = []; + } + this.option.modules = key; + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + setUsers(key, data) {//获取子组件值 + if(!key || key === ""){ + key = []; + } + this.option.users = key; + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + getOption(){ + return this.option; + } + }, + } +</script> + +<style scoped> + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 10px 0px; + } + + .ms-row { + margin: 0px 10px 0px; + } + + .ms-card { + width: 480px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue b/frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue new file mode 100644 index 0000000000..78613a4f3a --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue @@ -0,0 +1,77 @@ +<template> + <div v-loading="loading" class="ms-div"> + <el-card :style="{ width: w+'px'}"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span>{{$t('commons.report_statistics.excel')}} </p> + </el-row> + <el-row> + <el-table + :data="tableData" + :height="h" + :tree-props="{children: 'children', hasChildren: 'hasChildren'}" + row-key="id" + border + class="ms-table"> + <el-table-column + prop="name" + :label="$t('api_monitor.date')" + sortable> + </el-table-column> + <el-table-column + prop="createCount" + :label="$t('commons.report_statistics.add_case')" + sortable> + </el-table-column> + <el-table-column + prop="updateCount" + sortable + :label="$t('commons.report_statistics.change_case')"> + </el-table-column> + </el-table> + </el-row> + </el-card> + </div> +</template> + +<script> + export default { + name: "TestAnalysisTable", + components: {}, + props: { + tableData: Array, + }, + data() { + return { + w: document.documentElement.clientWidth - 760, + h: document.body.clientHeight / 2.3 - 20, + loading: false, + } + }, + methods: {}, + } +</script> + +<style scoped> + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-div { + margin-bottom: 20px; + } + + .ms-table { + width: 95%; + margin: 20px; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/xpack b/frontend/src/business/components/xpack index d99efd7e4f..e0da1d3ef6 160000 --- a/frontend/src/business/components/xpack +++ b/frontend/src/business/components/xpack @@ -1 +1 @@ -Subproject commit d99efd7e4f70846553444065fab8159e65035525 +Subproject commit e0da1d3ef611b0901e42520f6d68a04cabec4c08 diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index e0e6684b0c..e03bc3969e 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -43,6 +43,7 @@ export default { annotation: 'Annotation', clear: 'Clear', save: 'Save', + save_as: 'Save as', update: 'Update', save_success: 'Saved successfully', delete_success: 'Deleted successfully', diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 7ad15e8b90..92ce7bcaf8 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -43,6 +43,7 @@ export default { annotation: '注释', clear: '清空', save: '保存', + save_as: '另存为', update: '更新', save_success: '保存成功', delete_success: '删除成功', diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 83ec904f64..522716d107 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -43,6 +43,7 @@ export default { annotation: '註釋', clear: '清空', save: '保存', + save_as: '另存為', update: '更新', save_success: '保存成功', delete_success: '刪除成功',