feat(工作台): 联调工作台测试计划一揽子率

This commit is contained in:
xinxin.wu 2024-11-18 18:45:05 +08:00 committed by 刘瑞斌
parent d4191a07d8
commit 954ecca484
14 changed files with 191 additions and 93 deletions

View File

@ -13,6 +13,8 @@ import type {
PassRateDataType,
SelectedCardItem,
WorkHomePageDetail,
WorkTestPlanDetail,
WorkTestPlanRageDetail,
} from '@/models/workbench/homePage';
import {
@ -45,6 +47,7 @@ import {
WorkProOverviewDetailUrl,
WorkReviewListUrl,
WorkScenarioCaseCountDetailUrl,
WorkTestPlanRageUrl,
WorkTodoBugListUrl,
WorkTodoPlanListUrl,
WorkTodoReviewListUrl,
@ -196,6 +199,10 @@ export function workPlanLegacyBug(data: WorkHomePageDetail) {
export function workApiCountCoverRage(projectId: string) {
return MSR.get<ApiCoverageData>({ url: WorkApiCountCoverRateUrl, params: projectId }, { ignoreCancelToken: true });
}
// 工作台-首页-测试计划数量
export function workTestPlanRage(data: WorkTestPlanDetail) {
return MSR.post<WorkTestPlanRageDetail>({ url: WorkTestPlanRageUrl, data }, { ignoreCancelToken: true });
}
// 待办-用例评审列表
export function workbenchTodoReviewList(data: TableQueryParams) {

View File

@ -30,3 +30,4 @@ 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'; // 工作台-首页-覆盖率
export const WorkTestPlanRageUrl = '/test-plan/rage'; // 工作台-首页-测试计划数

View File

@ -180,7 +180,7 @@ export const defaultValueMap: Record<string, any> = {
},
pass: {
defaultList: cloneDeep(defaultPass),
color: ['#D4D4D8', '#00C261'],
color: ['#00C261', '#D4D4D8'],
defaultName: 'workbench.homePage.passRate',
},
complete: {

View File

@ -39,6 +39,24 @@ export interface WorkHomePageDetail extends TableQueryParams {
organizationId: string;
}
export interface WorkTestPlanDetail {
dayNumber: number | string;
startTime: number | null;
endTime: number | null;
projectId: string;
}
export interface WorkTestPlanRageDetail {
unExecute: number;
executed: number;
passed: number;
notPassed: number;
finished: number;
running: number;
prepared: number;
archived: number;
errorCode: number;
}
export interface TimeFormParams {
dayNumber: number | string;
startTime: number;

View File

@ -176,7 +176,7 @@
return props.item.key === WorkCardEnum.API_CASE_COUNT
? {
name: t('workbench.homePage.caseExecutionRate'),
color: ['#00C261', '#EDEDF1'],
color: ['#EDEDF1', '#00C261'],
tooltipText: t('workbench.homePage.apiCaseCountExecuteRateTooltip'),
}
: {
@ -190,12 +190,12 @@
return props.item.key === WorkCardEnum.API_CASE_COUNT
? {
name: t('workbench.homePage.casePassedRate'),
color: ['#00C261', '#ED0303'],
color: ['#ED0303', '#00C261'],
tooltipText: t('workbench.homePage.apiCaseCountPassRateTooltip'),
}
: {
name: t('workbench.homePage.executionRate'),
color: ['#00C261', '#ED0303'],
color: ['#ED0303', '#00C261'],
tooltipText: t('workbench.homePage.scenarioCaseCountPassRateTooltip'),
};
});
@ -212,6 +212,7 @@
props.item.key === WorkCardEnum.API_CASE_COUNT ? unCoverWithApiCase : unCoverWithApiScenario;
const coverWithCase = WorkCardEnum.API_CASE_COUNT ? coverWithApiCase : coverWithApiScenario;
coverData.value = cloneDeep(initCoverRate);
coverData.value = [
{
value: Number(coverage.split('%')[0]),

View File

@ -115,7 +115,7 @@
const apiCountOptions = ref({});
const hasPermission = ref<boolean>(false);
function handleCoverData(detail: ApiCoverageData) {
async function handleCoverData(detail: ApiCoverageData) {
const { unCoverWithApiDefinition, coverWithApiDefinition, apiCoverage } = detail;
const coverData: {
name: string;
@ -139,8 +139,8 @@
hasPermission.value,
`${props.item.key}-cover`
);
coverValueList.value = coverList;
coverOptions.value = covOptions;
coverValueList.value = [...coverList];
coverOptions.value = { ...covOptions };
}
async function initApiCountRate() {
try {
@ -213,6 +213,15 @@
}
);
watch(
() => hasPermission.value,
() => {
if (props.cover) {
handleCoverData(props.cover);
}
}
);
watch(
() => props.status,
(val) => {

View File

@ -220,7 +220,7 @@
...item,
children: item.children.filter(
(child) =>
child.label.toLowerCase().includes(searchKeyword.value.toLowerCase()) &&
t(child.label).toLowerCase().includes(searchKeyword.value.toLowerCase()) &&
!innerSelectedIds.value.includes(child.value)
),
}));

View File

@ -138,20 +138,20 @@
function initOptions() {
const { name, color } = props.rateConfig;
if (props.hasPermission) {
options.value.series.data = props.data.slice(1);
options.value.series.data = [...props.data.slice(1)];
options.value.legend.formatter = (seriousName: string) => {
const item = props.data.find((e) => e.name === seriousName);
return `{a|${seriousName}} {b|${addCommasToNumber(item?.value || 0)}}`;
};
if (props.hasPermission) {
options.value.title.subtext = `${props.data[0].value ?? 0}%`;
} else {
options.value.series.data = [];
options.value.title.subtext = `-%`;
}
options.value.graphic.invisible = !!props.hasPermission;
options.value.tooltip.show = !!props.hasPermission;
options.value.title.text = name;
@ -168,6 +168,13 @@
}
);
watch(
() => props.hasPermission,
() => {
initOptions();
}
);
onMounted(() => {
initOptions();
});

View File

@ -50,10 +50,16 @@
import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.vue';
import { workTestPlanRage } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import type {
SelectedCardItem,
TimeFormParams,
WorkTestPlanDetail,
WorkTestPlanRageDetail,
} from '@/models/workbench/homePage';
import { handlePieData, handleUpdateTabPie } from '../utils';
@ -83,33 +89,6 @@
})
);
// TODO
const detail = ref<PassRateDataType>({
statusStatisticsMap: {
execute: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
pass: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
complete: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
},
statusPercentList: [
{ status: 'HTTP', count: 1, percentValue: '10%' },
{ status: 'TCP', count: 3, percentValue: '0%' },
{ status: 'BBB', count: 6, percentValue: '0%' },
],
errorCode: 109001,
});
const executionOptions = ref<Record<string, any>>({});
const passOptions = ref<Record<string, any>>({});
const completeOptions = ref<Record<string, any>>({});
@ -146,42 +125,112 @@
});
const testPlanCountOptions = ref({});
//
const hasPermission = ref<boolean>(false);
async function initTestPlanCount() {
try {
const { startTime, endTime, dayNumber } = timeForm.value;
const params = {
current: 1,
pageSize: 5,
const params: WorkTestPlanDetail = {
startTime: dayNumber ? null : startTime,
endTime: dayNumber ? null : endTime,
dayNumber: dayNumber ?? null,
projectIds: innerProjectIds.value,
organizationId: appStore.currentOrgId,
handleUsers: [],
projectId: innerProjectIds.value[0],
};
const { statusStatisticsMap, statusPercentList, errorCode } = detail.value;
const detail: WorkTestPlanRageDetail = await workTestPlanRage(params);
const { unExecute, executed, passed, notPassed, finished, running, prepared, archived, errorCode } = detail;
hasPermission.value = errorCode !== 109001;
testPlanCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
const executeRate =
executed + unExecute > 0 ? parseFloat(((executed / (executed + unExecute)) * 100).toFixed(2)) : 0;
const executeData: {
name: string;
count: number;
}[] = [
{
name: t('workbench.homePage.executeRate'),
count: executeRate,
},
{
name: t('common.unExecute'),
count: unExecute,
},
{
name: t('common.executed'),
count: executed,
},
];
const passRate = passed + notPassed > 0 ? parseFloat(((passed / (passed + notPassed)) * 100).toFixed(2)) : 0;
const passData = [
{
name: t('workbench.homePage.passRate'),
count: passRate,
},
{
name: t('workbench.homePage.havePassed'),
count: passed,
},
{
name: t('workbench.homePage.notPass'),
count: notPassed,
},
];
const statusPercentList = [
{ status: t('common.notStarted'), count: prepared, percentValue: '0%' },
{ status: t('common.inProgress'), count: running, percentValue: '0%' },
{ status: t('common.completed'), count: finished, percentValue: '0%' },
{ status: t('common.archived'), count: archived, percentValue: '0%' },
];
const total = statusPercentList.reduce((sum, item) => sum + item.count, 0);
const listStatusPercentList = statusPercentList.map((item) => ({
...item,
percentValue: total > 0 ? `${((item.count / total) * 100).toFixed(2)}%` : '0%',
}));
const completeRate = total > 0 ? parseFloat(((finished / total) * 100).toFixed(2)) : 0;
const completeData = [
{
name: t('workbench.homePage.completeRate'),
count: completeRate,
},
{
name: t('common.completed'),
count: finished,
},
{
name: t('common.inProgress'),
count: running,
},
{
name: t('common.notStarted'),
count: prepared,
},
];
testPlanCountOptions.value = handlePieData(props.item.key, hasPermission.value, listStatusPercentList);
//
const { options: executedOptions, valueList: executedList } = handleUpdateTabPie(
statusStatisticsMap?.execute || [],
executeData,
hasPermission.value,
`${props.item.key}-execute`
);
//
const { options: passedOptions, valueList: passList } = handleUpdateTabPie(
statusStatisticsMap?.pass || [],
passData,
hasPermission.value,
`${props.item.key}-pass`
);
//
const { options: comOptions, valueList: completeList } = handleUpdateTabPie(
statusStatisticsMap?.complete || [],
completeData,
hasPermission.value,
`${props.item.key}-complete`
);

View File

@ -95,7 +95,7 @@
:type="item.key"
:item="item"
:status="projectLoadingStatus[item.projectIds[0]]"
:cover="requestResults.get(item.projectIds[0])"
:cover="requestResults[item.projectIds[0]]"
@change="changeHandler"
/>
<ApiChangeList
@ -123,7 +123,7 @@
v-model:projectIds="item.projectIds"
:status="projectLoadingStatus[item.projectIds[0]]"
:item="item"
:cover="requestResults.get(item.projectIds[0])"
:cover="requestResults[item.projectIds[0]]"
@change="changeHandler"
/>
<TestPlanCount
@ -173,7 +173,7 @@
import { sleep } from '@/utils';
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
import { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { ApiCoverageData, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { WorkCardEnum } from '@/enums/workbenchEnum';
const userStore = useUserStore();
@ -235,33 +235,9 @@
const defaultWorkList = ref<SelectedCardItem[]>([]);
const showNoData = ref(false);
async function initDefaultList() {
try {
appStore.showLoading();
const result = await getDashboardLayout(appStore.currentOrgId);
defaultWorkList.value = result;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
if (!defaultWorkList.value.length) {
showNoData.value = true;
}
appStore.hideLoading();
}
}
async function changeHandler() {
try {
await editDashboardLayout(defaultWorkList.value, appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
// key ID
const requestResults = ref(new Map());
const requestResults = ref<Record<string, ApiCoverageData>>({});
//
const requestedIds = ref(new Set());
@ -271,7 +247,7 @@
try {
projectLoadingStatus.value[projectId] = true;
const result = await workApiCountCoverRage(projectId);
requestResults.value.set(projectId, result);
requestResults.value[projectId] = result;
projectLoadingStatus.value[projectId] = false;
await sleep(300);
} catch (error) {
@ -285,6 +261,8 @@
// id
async function requestQueue() {
requestedIds.value = new Set([]);
requestResults.value = {};
const awaitType = [WorkCardEnum.API_COUNT, WorkCardEnum.API_CASE_COUNT, WorkCardEnum.SCENARIO_COUNT];
const queueList = defaultWorkList.value.filter((item) => awaitType.includes(item.key));
for (let i = 0; i < queueList.length; i++) {
@ -297,13 +275,40 @@
}
}
//
async function handleRefresh() {
await initDefaultList();
async function initDefaultList() {
if (appStore.currentOrgId) {
try {
appStore.showLoading();
const result = await getDashboardLayout(appStore.currentOrgId);
defaultWorkList.value = result;
requestQueue();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
if (!defaultWorkList.value.length) {
showNoData.value = true;
}
appStore.hideLoading();
}
}
}
onMounted(async () => {
async function changeHandler() {
try {
await editDashboardLayout(defaultWorkList.value, appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function handleRefresh() {
initDefaultList();
}
onMounted(() => {
const defaultTime = getLocalStorage(`WORK_TIME_${userStore.id}`);
if (!defaultTime) {
setLocalStorage(`WORK_TIME_${userStore.id}`, JSON.stringify(timeForm.value));
@ -314,8 +319,7 @@
rangeTime.value = [startTime, endTime];
}
}
await initDefaultList();
requestQueue();
initDefaultList();
});
const time = ref({ ...timeForm.value });

View File

@ -42,6 +42,7 @@ export default {
'workbench.homePage.associationCASE': 'Association CASE',
'workbench.homePage.associatedScene': 'Associated scenarios',
'workbench.homePage.pendingDefect': 'Defects for me to deal with',
'workbench.homePage.defectProcessingNumber': 'Statistics of defect handlers',
'workbench.homePage.defectTotal': 'Total defect',
'workbench.homePage.legacyDefectsNumber': 'Legacy defects number',
'workbench.homePage.createdBugByMe': 'My created defects',

View File

@ -42,7 +42,7 @@ export default {
'workbench.homePage.associationCASE': '关联CASE',
'workbench.homePage.associatedScene': '关联场景',
'workbench.homePage.pendingDefect': '待我处理的缺陷',
'workbench.homePage.defectProcessingNumber': '缺陷处理人',
'workbench.homePage.defectProcessingNumber': '缺陷处理人统计',
'workbench.homePage.defectTotal': '缺陷总数',
'workbench.homePage.legacyDefectsNumber': '遗留缺陷数',
'workbench.homePage.createdBugByMe': '我创建的缺陷',

View File

@ -446,6 +446,7 @@ export function handlePieData(
options.title.subtext = addCommasToNumber(totalCount);
if (!hasPermission) {
options.title.subtext = '-';
options.series.data = [];
}
// 设置图例的格式化函数,显示百分比

View File

@ -16,12 +16,12 @@
v-model:model-value="features"
:options="featureOptions"
:allow-search="false"
allow-clear
class="!w-[240px]"
:prefix="t('project.messageManagement.function')"
:multiple="true"
:has-all-select="true"
:default-all-select="true"
:at-least-one="true"
/>
<a-button type="outline" class="arco-btn-outline--secondary p-[10px]" @click="() => handleRefresh()">
<MsIcon type="icon-icon_reset_outlined" size="14" />