diff --git a/frontend/src/api/modules/workbench.ts b/frontend/src/api/modules/workbench.ts index deca52b2fa..dcb4767cea 100644 --- a/frontend/src/api/modules/workbench.ts +++ b/frontend/src/api/modules/workbench.ts @@ -8,6 +8,7 @@ import type { CaseManagementTable } from '@/models/caseManagement/featureCase'; import type { CommonList, TableQueryParams } from '@/models/common'; import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan'; import type { + ApiCoverageData, OverViewOfProject, PassRateDataType, SelectedCardItem, @@ -19,6 +20,7 @@ import { GetDashboardLayoutUrl, WorkApiCaseCountDetailUrl, WorkApiChangeListUrl, + WorkApiCountCoverRateUrl, WorkApiCountDetailUrl, WorkAssociateCaseDetailUrl, WorkbenchApiCaseListUrl, @@ -37,6 +39,7 @@ import { WorkHandleUserOptionsUrl, WorkMemberViewDetailUrl, WorkMyCreatedDetailUrl, + WorkPlanLegacyBugUrl, WorkProOverviewDetailUrl, WorkReviewListUrl, WorkScenarioCaseCountDetailUrl, @@ -172,6 +175,16 @@ export function workHandleUserOptions(projectId: string) { return MSR.get({ url: WorkHandleUserOptionsUrl, params: projectId }, { ignoreCancelToken: true }); } +// 工作台-首页-测试计划遗留缺陷 +export function workPlanLegacyBug(data: WorkHomePageDetail) { + return MSR.post({ url: WorkPlanLegacyBugUrl, data }, { ignoreCancelToken: true }); +} + +// 工作台-首页-接口测试覆盖率 +export function workApiCountCoverRage(projectId: string) { + return MSR.get({ url: WorkApiCountCoverRateUrl, params: projectId }, { ignoreCancelToken: true }); +} + // 待办-用例评审列表 export function workbenchTodoReviewList(data: TableQueryParams) { return MSR.post>({ url: WorkTodoReviewListUrl, data }); diff --git a/frontend/src/api/requrls/workbench.ts b/frontend/src/api/requrls/workbench.ts index dbc6f7fce3..71e5b1f482 100644 --- a/frontend/src/api/requrls/workbench.ts +++ b/frontend/src/api/requrls/workbench.ts @@ -26,3 +26,5 @@ export const WorkHandleUserOptionsUrl = '/dashboard/bug_handle_user/list'; // export const WorkBugCountDetailUrl = '/dashboard/bug_count'; // 工作台-首页-缺陷数量 export const WorkBugByMeCreatedUrl = '/dashboard/create_bug_by_me'; // 工作台-首页-我创建的缺陷 export const WorkBugHandleByMeUrl = '/dashboard/handle_bug_by_me'; // 工作台-首页-待我处理的缺陷 +export const WorkPlanLegacyBugUrl = '/dashboard/plan_legacy_bug'; // 工作台-首页-测试计划遗留缺陷 +export const WorkApiCountCoverRateUrl = '/api/definition/rage'; // 工作台-首页-覆盖率 diff --git a/frontend/src/config/workbench.ts b/frontend/src/config/workbench.ts index 843efc07fe..70f3a9109b 100644 --- a/frontend/src/config/workbench.ts +++ b/frontend/src/config/workbench.ts @@ -62,12 +62,12 @@ export const defaultCover = [ { label: 'workbench.homePage.covered', value: '-', - name: '', + name: 'workbench.homePage.covered', }, { label: 'workbench.homePage.notCover', value: '-', - name: '', + name: 'workbench.homePage.notCover', }, ]; // 评审率 diff --git a/frontend/src/models/workbench/homePage.ts b/frontend/src/models/workbench/homePage.ts index 01ee3077a2..4a45abee8a 100644 --- a/frontend/src/models/workbench/homePage.ts +++ b/frontend/src/models/workbench/homePage.ts @@ -83,3 +83,18 @@ export interface PassRateDataType { | null; errorCode: number; } + +export interface ApiCoverageData { + allApiCount: number; // 总的 API 数量 + unCoverWithApiDefinition: number; // 未覆盖 API 定义的数量 + coverWithApiDefinition: number; // 覆盖了 API 定义的数量 + apiCoverage: string; // API 覆盖率 + + unCoverWithApiCase: number; // 未覆盖 API 测试用例的数量 + coverWithApiCase: number; // 覆盖了 API 测试用例的数量 + apiCaseCoverage: string; // API 测试用例覆盖率( + + unCoverWithApiScenario: number; // 未覆盖 API 场景的数量 + coverWithApiScenario: number; // 覆盖了 API 场景的数量 + scenarioCoverage: string; // API 场景覆盖率 +} diff --git a/frontend/src/views/workbench/homePage/components/apiAndScenarioCase.vue b/frontend/src/views/workbench/homePage/components/apiAndScenarioCase.vue index 838833a8c1..7baf8d559c 100644 --- a/frontend/src/views/workbench/homePage/components/apiAndScenarioCase.vue +++ b/frontend/src/views/workbench/homePage/components/apiAndScenarioCase.vue @@ -36,7 +36,12 @@
- +
@@ -54,16 +59,17 @@ * @desc 接口用例数量/场景用例数量 */ import { ref } from 'vue'; + import { cloneDeep } from 'lodash-es'; import MsSelect from '@/components/business/ms-select'; import RatioPie from './ratioPie.vue'; - import { workApiCaseCountDetail, workScenarioCaseCountDetail } from '@/api/modules/workbench'; + import { workApiCaseCountDetail, workApiCountCoverRage, workScenarioCaseCountDetail } from '@/api/modules/workbench'; import { useI18n } from '@/hooks/useI18n'; import useAppStore from '@/store/modules/app'; import { addCommasToNumber } from '@/utils'; - import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage'; + import type { ApiCoverageData, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage'; import { WorkCardEnum } from '@/enums/workbenchEnum'; const appStore = useAppStore(); @@ -72,6 +78,8 @@ const props = defineProps<{ item: SelectedCardItem; + status?: boolean; + cover?: ApiCoverageData; }>(); const emit = defineEmits<{ @@ -86,7 +94,7 @@ const executionTimeValue = ref<{ name: string; count: number | string }[]>([ { - name: '执行次数', + name: t('workbench.homePage.executionTimes'), count: '-', }, ]); @@ -114,8 +122,11 @@ }) ); - // 接口覆盖 - const coverData = ref<{ name: string; value: number }[]>([ + const initCoverRate = [ + { + value: 0, + name: t('workbench.homePage.coverRate'), + }, { value: 0, name: t('workbench.homePage.notCover'), @@ -124,7 +135,10 @@ value: 0, name: t('workbench.homePage.covered'), }, - ]); + ]; + + // 接口覆盖 + const coverData = ref<{ name: string; value: number }[]>(cloneDeep(initCoverRate)); const caseExecuteData = ref<{ name: string; value: number }[]>([ { value: 0, @@ -187,8 +201,49 @@ }); const hasPermission = ref(false); + const loading = ref(false); + + function handleCoverData(detail: ApiCoverageData) { + const { unCoverWithApiCase, coverWithApiCase, apiCaseCoverage } = detail; + const { unCoverWithApiScenario, coverWithApiScenario, scenarioCoverage } = detail; + + const coverage = props.item.key === WorkCardEnum.API_CASE_COUNT ? apiCaseCoverage : scenarioCoverage; + const unCoverWithCase = + props.item.key === WorkCardEnum.API_CASE_COUNT ? unCoverWithApiCase : unCoverWithApiScenario; + + const coverWithCase = WorkCardEnum.API_CASE_COUNT ? coverWithApiCase : coverWithApiScenario; + coverData.value = [ + { + value: Number(coverage.split('%')[0]), + name: t('workbench.homePage.coverRate'), + }, + { + value: unCoverWithCase, + name: t('workbench.homePage.notCover'), + }, + { + value: coverWithCase, + name: t('workbench.homePage.covered'), + }, + ]; + } + + async function initApiCountRate() { + try { + loading.value = true; + const detail = await workApiCountCoverRage(projectId.value); + handleCoverData(detail); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } finally { + loading.value = false; + } + } + async function initApiOrScenarioCount() { try { + coverData.value = cloneDeep(initCoverRate); const { startTime, endTime, dayNumber } = timeForm.value; const params = { @@ -241,6 +296,7 @@ nextTick(() => { initApiOrScenarioCount(); emit('change'); + initApiCountRate(); }); } @@ -248,6 +304,26 @@ initApiOrScenarioCount(); }); + watch( + () => props.cover, + (val) => { + if (val) { + handleCoverData(val); + } + }, + { + deep: true, + immediate: true, + } + ); + + watch( + () => props.status, + (val) => { + loading.value = val; + } + ); + watch( () => innerProjectIds.value, (val) => { diff --git a/frontend/src/views/workbench/homePage/components/apiCount.vue b/frontend/src/views/workbench/homePage/components/apiCount.vue index 9b0cee355b..8a30eb572f 100644 --- a/frontend/src/views/workbench/homePage/components/apiCount.vue +++ b/frontend/src/views/workbench/homePage/components/apiCount.vue @@ -25,6 +25,7 @@ :tooltip-text="tabItem.tooltip" :options="tabItem.options" :size="60" + :loading="tabItem.value === 'cover' ? loading : undefined" :has-permission="hasPermission" :value-list="tabItem.valueList" /> @@ -49,11 +50,11 @@ import PassRatePie from './passRatePie.vue'; import TabCard from './tabCard.vue'; - import { workApiCountDetail } from '@/api/modules/workbench'; + import { workApiCountCoverRage, workApiCountDetail } from '@/api/modules/workbench'; import { useI18n } from '@/hooks/useI18n'; import useAppStore from '@/store/modules/app'; - import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage'; + import type { ApiCoverageData, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage'; import { handlePieData, handleUpdateTabPie } from '../utils'; @@ -63,6 +64,8 @@ const props = defineProps<{ item: SelectedCardItem; projectIds: string[]; + status?: boolean; + cover?: ApiCoverageData; }>(); const emit = defineEmits<{ @@ -73,6 +76,8 @@ required: true, }); + const loading = ref(false); + const projectId = ref(innerProjectIds.value[0]); const timeForm = inject>( @@ -93,14 +98,14 @@ return [ { label: '', - value: 'execution', + value: 'cover', valueList: coverValueList.value, options: { ...coverOptions.value }, tooltip: 'workbench.homePage.apiCountCoverRateTooltip', }, { label: '', - value: 'pass', + value: 'complete', valueList: completeValueList.value, options: { ...completeOptions.value }, tooltip: 'workbench.homePage.apiCountCompleteRateTooltip', @@ -109,8 +114,47 @@ }); const apiCountOptions = ref({}); - const hasPermission = ref(false); + function handleCoverData(detail: ApiCoverageData) { + const { unCoverWithApiDefinition, coverWithApiDefinition, apiCoverage } = detail; + const coverData: { + name: string; + count: number; + }[] = [ + { + count: Number(apiCoverage.split('%')[0]), + name: t('workbench.homePage.coverRate'), + }, + { + count: coverWithApiDefinition, + name: t('workbench.homePage.covered'), + }, + { + count: unCoverWithApiDefinition, + name: t('workbench.homePage.notCover'), + }, + ]; + const { options: covOptions, valueList: coverList } = handleUpdateTabPie( + coverData, + hasPermission.value, + `${props.item.key}-cover` + ); + coverValueList.value = coverList; + coverOptions.value = covOptions; + } + async function initApiCountRate() { + try { + loading.value = true; + const detail = await workApiCountCoverRage(projectId.value); + handleCoverData(detail); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } finally { + loading.value = false; + } + } + async function initApiCount() { try { const { startTime, endTime, dayNumber } = timeForm.value; @@ -127,18 +171,8 @@ const { statusStatisticsMap, statusPercentList, errorCode } = detail; hasPermission.value = errorCode !== 109001; - apiCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList); - // 覆盖率 TODO 等接口 - // const { options: covOptions, valueList: coverList } = handleUpdateTabPie( - // statusStatisticsMap?.cover || [], - // hasPermission.value, - // `${props.item.key}-cover` - // ); - // coverValueList.value = coverList; - // coverOptions.value = covOptions; - // 完成率 const { options: comOptions, valueList: completedList } = handleUpdateTabPie( statusStatisticsMap?.completionRate || [], @@ -157,6 +191,7 @@ nextTick(() => { initApiCount(); emit('change'); + initApiCountRate(); }); } @@ -165,6 +200,26 @@ emit('change'); }); + watch( + () => props.cover, + (val) => { + if (val) { + handleCoverData(val); + } + }, + { + deep: true, + immediate: true, + } + ); + + watch( + () => props.status, + (val) => { + loading.value = val; + } + ); + watch( () => innerProjectIds.value, (val) => { diff --git a/frontend/src/views/workbench/homePage/components/cardSettingDrawer.vue b/frontend/src/views/workbench/homePage/components/cardSettingDrawer.vue index 6445c31d47..d4b276118c 100644 --- a/frontend/src/views/workbench/homePage/components/cardSettingDrawer.vue +++ b/frontend/src/views/workbench/homePage/components/cardSettingDrawer.vue @@ -21,7 +21,7 @@
- +
@@ -131,8 +131,11 @@ const defaultAllProjectType = [WorkCardEnum.PROJECT_VIEW, WorkCardEnum.CREATE_BY_ME]; + const selectedIds = ref([]); + // 添加卡片 function addCard(item: childrenWorkConfigItem) { + selectedIds.value.push(item.value); const newCard: SelectedCardItem = { id: getGenerateId(), fullScreen: !!isFullScreenType.includes(item.value), @@ -153,6 +156,7 @@ function deleteHandler(item: SelectedCardItem) { const index = selectedCardList.value.findIndex((e) => e.id === item.id); selectedCardList.value.splice(index, 1); + selectedIds.value.splice(index, 1); } const confirmLoading = ref(false); @@ -183,6 +187,7 @@ (val) => { if (val) { selectedCardList.value = cloneDeep(props.list); + selectedIds.value = props.list.map((e) => e.key); } }, { diff --git a/frontend/src/views/workbench/homePage/components/cardSettingList.vue b/frontend/src/views/workbench/homePage/components/cardSettingList.vue index a09baf2a97..fcd5421550 100644 --- a/frontend/src/views/workbench/homePage/components/cardSettingList.vue +++ b/frontend/src/views/workbench/homePage/components/cardSettingList.vue @@ -59,6 +59,10 @@ (e: 'add', item: childrenWorkConfigItem): void; }>(); + const innerSelectedIds = defineModel('selectedIds', { + required: true, + }); + const configList = ref([ // 概览 { @@ -212,17 +216,14 @@ const searchKeyword = ref(''); // 存储搜索关键字 const filteredConfigList = computed(() => { - if (!searchKeyword.value) { - return configList.value; - } - return configList.value - .map((item) => ({ - ...item, - children: item.children.filter((child) => - child.label.toLowerCase().includes(searchKeyword.value.toLowerCase()) - ), - })) - .filter((item) => item.children.length > 0); + return configList.value.map((item) => ({ + ...item, + children: item.children.filter( + (child) => + child.label.toLowerCase().includes(searchKeyword.value.toLowerCase()) && + !innerSelectedIds.value.includes(child.value) + ), + })); }); // 搜索 diff --git a/frontend/src/views/workbench/homePage/components/defectCount.vue b/frontend/src/views/workbench/homePage/components/defectCount.vue index 63d37e6c0d..3c455a1d00 100644 --- a/frontend/src/views/workbench/homePage/components/defectCount.vue +++ b/frontend/src/views/workbench/homePage/components/defectCount.vue @@ -46,7 +46,12 @@ import MsSelect from '@/components/business/ms-select'; import PassRatePie from './passRatePie.vue'; - import { workBugByMeCreated, workBugCountDetail, workBugHandleByMe } from '@/api/modules/workbench'; + import { + workBugByMeCreated, + workBugCountDetail, + workBugHandleByMe, + workPlanLegacyBug, + } from '@/api/modules/workbench'; import { useI18n } from '@/hooks/useI18n'; import useAppStore from '@/store/modules/app'; @@ -93,12 +98,17 @@ const countOptions = ref>({}); - type SelectedBugCountKeys = WorkCardEnum.BUG_COUNT | WorkCardEnum.HANDLE_BUG_BY_ME | WorkCardEnum.CREATE_BUG_BY_ME; + type SelectedBugCountKeys = + | WorkCardEnum.BUG_COUNT + | WorkCardEnum.HANDLE_BUG_BY_ME + | WorkCardEnum.CREATE_BUG_BY_ME + | WorkCardEnum.PLAN_LEGACY_BUG; const currentBugCount: (data: WorkHomePageDetail) => Promise = { [WorkCardEnum.BUG_COUNT]: workBugCountDetail, [WorkCardEnum.HANDLE_BUG_BY_ME]: workBugHandleByMe, [WorkCardEnum.CREATE_BUG_BY_ME]: workBugByMeCreated, + [WorkCardEnum.PLAN_LEGACY_BUG]: workPlanLegacyBug, }[props.item.key as SelectedBugCountKeys]; const hasPermission = ref(false); diff --git a/frontend/src/views/workbench/homePage/components/overview.vue b/frontend/src/views/workbench/homePage/components/overview.vue index d13825cda3..7e4f05c46d 100644 --- a/frontend/src/views/workbench/homePage/components/overview.vue +++ b/frontend/src/views/workbench/homePage/components/overview.vue @@ -27,7 +27,7 @@
- +
@@ -115,9 +115,11 @@ // x轴 options.value.xAxis.data = cardModuleList.value.map((e) => e.label); + let maxAxis = 5; + // 处理data数据 options.value.series = detail.projectCountList.map((item) => { - const countData = item.count.map((e) => { + const countData: Record = item.count.map((e) => { return { name: item.name, value: e !== 0 ? e : undefined, @@ -142,6 +144,11 @@ }, }; }); + + const itemMax = Math.max(...item.count); + + maxAxis = Math.max(itemMax, maxAxis); + return { name: item.name, type: 'bar', @@ -154,6 +161,10 @@ data: countData, }; }); + options.value.yAxis[0].max = maxAxis < 100 ? 50 : maxAxis + 50; + + options.value.series[0].barGap = 4; + options.value.series[0].barCategoryGap = 24; } async function initOverViewDetail() { @@ -178,6 +189,7 @@ handleData(detail); } catch (error) { + // eslint-disable-next-line no-console console.log(error); } } diff --git a/frontend/src/views/workbench/homePage/components/overviewMember.vue b/frontend/src/views/workbench/homePage/components/overviewMember.vue index 18ea4c46d5..d576e7f79f 100644 --- a/frontend/src/views/workbench/homePage/components/overviewMember.vue +++ b/frontend/src/views/workbench/homePage/components/overviewMember.vue @@ -98,7 +98,11 @@ options.value.graphic.invisible = invisible; options.value.graphic.style.text = text; options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10)); + let maxAxis = 5; options.value.series = detail.projectCountList.map((item, index) => { + const itemMax = Math.max(...item.count); + + maxAxis = Math.max(itemMax, maxAxis); return { name: t(contentTabList[index].label), type: 'bar', @@ -110,6 +114,7 @@ }, }; }); + options.value.yAxis[0].max = maxAxis < 100 ? 50 : maxAxis + 50; } async function initOverViewMemberDetail() { diff --git a/frontend/src/views/workbench/homePage/components/passRatePie.vue b/frontend/src/views/workbench/homePage/components/passRatePie.vue index f602133007..6b6854d38a 100644 --- a/frontend/src/views/workbench/homePage/components/passRatePie.vue +++ b/frontend/src/views/workbench/homePage/components/passRatePie.vue @@ -1,5 +1,5 @@