feat(工作台): 工作台首页联调部分卡片接口覆盖率

This commit is contained in:
xinxin.wu 2024-11-15 17:48:20 +08:00 committed by Craftsman
parent e2ef7e8c93
commit cbc8cd8a7a
15 changed files with 309 additions and 62 deletions

View File

@ -8,6 +8,7 @@ import type { CaseManagementTable } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common'; import type { CommonList, TableQueryParams } from '@/models/common';
import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan'; import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan';
import type { import type {
ApiCoverageData,
OverViewOfProject, OverViewOfProject,
PassRateDataType, PassRateDataType,
SelectedCardItem, SelectedCardItem,
@ -19,6 +20,7 @@ import {
GetDashboardLayoutUrl, GetDashboardLayoutUrl,
WorkApiCaseCountDetailUrl, WorkApiCaseCountDetailUrl,
WorkApiChangeListUrl, WorkApiChangeListUrl,
WorkApiCountCoverRateUrl,
WorkApiCountDetailUrl, WorkApiCountDetailUrl,
WorkAssociateCaseDetailUrl, WorkAssociateCaseDetailUrl,
WorkbenchApiCaseListUrl, WorkbenchApiCaseListUrl,
@ -37,6 +39,7 @@ import {
WorkHandleUserOptionsUrl, WorkHandleUserOptionsUrl,
WorkMemberViewDetailUrl, WorkMemberViewDetailUrl,
WorkMyCreatedDetailUrl, WorkMyCreatedDetailUrl,
WorkPlanLegacyBugUrl,
WorkProOverviewDetailUrl, WorkProOverviewDetailUrl,
WorkReviewListUrl, WorkReviewListUrl,
WorkScenarioCaseCountDetailUrl, WorkScenarioCaseCountDetailUrl,
@ -172,6 +175,16 @@ export function workHandleUserOptions(projectId: string) {
return MSR.get({ url: WorkHandleUserOptionsUrl, params: projectId }, { ignoreCancelToken: true }); return MSR.get({ url: WorkHandleUserOptionsUrl, params: projectId }, { ignoreCancelToken: true });
} }
// 工作台-首页-测试计划遗留缺陷
export function workPlanLegacyBug(data: WorkHomePageDetail) {
return MSR.post<PassRateDataType>({ url: WorkPlanLegacyBugUrl, data }, { ignoreCancelToken: true });
}
// 工作台-首页-接口测试覆盖率
export function workApiCountCoverRage(projectId: string) {
return MSR.get<ApiCoverageData>({ url: WorkApiCountCoverRateUrl, params: projectId }, { ignoreCancelToken: true });
}
// 待办-用例评审列表 // 待办-用例评审列表
export function workbenchTodoReviewList(data: TableQueryParams) { export function workbenchTodoReviewList(data: TableQueryParams) {
return MSR.post<CommonList<ReviewItem>>({ url: WorkTodoReviewListUrl, data }); return MSR.post<CommonList<ReviewItem>>({ url: WorkTodoReviewListUrl, data });

View File

@ -26,3 +26,5 @@ export const WorkHandleUserOptionsUrl = '/dashboard/bug_handle_user/list'; //
export const WorkBugCountDetailUrl = '/dashboard/bug_count'; // 工作台-首页-缺陷数量 export const WorkBugCountDetailUrl = '/dashboard/bug_count'; // 工作台-首页-缺陷数量
export const WorkBugByMeCreatedUrl = '/dashboard/create_bug_by_me'; // 工作台-首页-我创建的缺陷 export const WorkBugByMeCreatedUrl = '/dashboard/create_bug_by_me'; // 工作台-首页-我创建的缺陷
export const WorkBugHandleByMeUrl = '/dashboard/handle_bug_by_me'; // 工作台-首页-待我处理的缺陷 export const WorkBugHandleByMeUrl = '/dashboard/handle_bug_by_me'; // 工作台-首页-待我处理的缺陷
export const WorkPlanLegacyBugUrl = '/dashboard/plan_legacy_bug'; // 工作台-首页-测试计划遗留缺陷
export const WorkApiCountCoverRateUrl = '/api/definition/rage'; // 工作台-首页-覆盖率

View File

@ -62,12 +62,12 @@ export const defaultCover = [
{ {
label: 'workbench.homePage.covered', label: 'workbench.homePage.covered',
value: '-', value: '-',
name: '', name: 'workbench.homePage.covered',
}, },
{ {
label: 'workbench.homePage.notCover', label: 'workbench.homePage.notCover',
value: '-', value: '-',
name: '', name: 'workbench.homePage.notCover',
}, },
]; ];
// 评审率 // 评审率

View File

@ -83,3 +83,18 @@ export interface PassRateDataType {
| null; | null;
errorCode: number; 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 场景覆盖率
}

View File

@ -36,7 +36,12 @@
</div> </div>
<div class="case-ratio-wrapper mt-[16px]"> <div class="case-ratio-wrapper mt-[16px]">
<div class="case-ratio-item"> <div class="case-ratio-item">
<RatioPie :has-permission="hasPermission" :data="coverData" :rate-config="coverTitleConfig" /> <RatioPie
:has-permission="hasPermission"
:loading="loading"
:data="coverData"
:rate-config="coverTitleConfig"
/>
</div> </div>
<div class="case-ratio-item"> <div class="case-ratio-item">
<RatioPie :has-permission="hasPermission" :data="caseExecuteData" :rate-config="executeTitleConfig" /> <RatioPie :has-permission="hasPermission" :data="caseExecuteData" :rate-config="executeTitleConfig" />
@ -54,16 +59,17 @@
* @desc 接口用例数量/场景用例数量 * @desc 接口用例数量/场景用例数量
*/ */
import { ref } from 'vue'; import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import RatioPie from './ratioPie.vue'; 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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { addCommasToNumber } from '@/utils'; 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'; import { WorkCardEnum } from '@/enums/workbenchEnum';
const appStore = useAppStore(); const appStore = useAppStore();
@ -72,6 +78,8 @@
const props = defineProps<{ const props = defineProps<{
item: SelectedCardItem; item: SelectedCardItem;
status?: boolean;
cover?: ApiCoverageData;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -86,7 +94,7 @@
const executionTimeValue = ref<{ name: string; count: number | string }[]>([ const executionTimeValue = ref<{ name: string; count: number | string }[]>([
{ {
name: '执行次数', name: t('workbench.homePage.executionTimes'),
count: '-', count: '-',
}, },
]); ]);
@ -114,8 +122,11 @@
}) })
); );
// const initCoverRate = [
const coverData = ref<{ name: string; value: number }[]>([ {
value: 0,
name: t('workbench.homePage.coverRate'),
},
{ {
value: 0, value: 0,
name: t('workbench.homePage.notCover'), name: t('workbench.homePage.notCover'),
@ -124,7 +135,10 @@
value: 0, value: 0,
name: t('workbench.homePage.covered'), name: t('workbench.homePage.covered'),
}, },
]); ];
//
const coverData = ref<{ name: string; value: number }[]>(cloneDeep(initCoverRate));
const caseExecuteData = ref<{ name: string; value: number }[]>([ const caseExecuteData = ref<{ name: string; value: number }[]>([
{ {
value: 0, value: 0,
@ -187,8 +201,49 @@
}); });
const hasPermission = ref<boolean>(false); const hasPermission = ref<boolean>(false);
const loading = ref<boolean>(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() { async function initApiOrScenarioCount() {
try { try {
coverData.value = cloneDeep(initCoverRate);
const { startTime, endTime, dayNumber } = timeForm.value; const { startTime, endTime, dayNumber } = timeForm.value;
const params = { const params = {
@ -241,6 +296,7 @@
nextTick(() => { nextTick(() => {
initApiOrScenarioCount(); initApiOrScenarioCount();
emit('change'); emit('change');
initApiCountRate();
}); });
} }
@ -248,6 +304,26 @@
initApiOrScenarioCount(); initApiOrScenarioCount();
}); });
watch(
() => props.cover,
(val) => {
if (val) {
handleCoverData(val);
}
},
{
deep: true,
immediate: true,
}
);
watch(
() => props.status,
(val) => {
loading.value = val;
}
);
watch( watch(
() => innerProjectIds.value, () => innerProjectIds.value,
(val) => { (val) => {

View File

@ -25,6 +25,7 @@
:tooltip-text="tabItem.tooltip" :tooltip-text="tabItem.tooltip"
:options="tabItem.options" :options="tabItem.options"
:size="60" :size="60"
:loading="tabItem.value === 'cover' ? loading : undefined"
:has-permission="hasPermission" :has-permission="hasPermission"
:value-list="tabItem.valueList" :value-list="tabItem.valueList"
/> />
@ -49,11 +50,11 @@
import PassRatePie from './passRatePie.vue'; import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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'; import { handlePieData, handleUpdateTabPie } from '../utils';
@ -63,6 +64,8 @@
const props = defineProps<{ const props = defineProps<{
item: SelectedCardItem; item: SelectedCardItem;
projectIds: string[]; projectIds: string[];
status?: boolean;
cover?: ApiCoverageData;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -73,6 +76,8 @@
required: true, required: true,
}); });
const loading = ref<boolean>(false);
const projectId = ref<string>(innerProjectIds.value[0]); const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>( const timeForm = inject<Ref<TimeFormParams>>(
@ -93,14 +98,14 @@
return [ return [
{ {
label: '', label: '',
value: 'execution', value: 'cover',
valueList: coverValueList.value, valueList: coverValueList.value,
options: { ...coverOptions.value }, options: { ...coverOptions.value },
tooltip: 'workbench.homePage.apiCountCoverRateTooltip', tooltip: 'workbench.homePage.apiCountCoverRateTooltip',
}, },
{ {
label: '', label: '',
value: 'pass', value: 'complete',
valueList: completeValueList.value, valueList: completeValueList.value,
options: { ...completeOptions.value }, options: { ...completeOptions.value },
tooltip: 'workbench.homePage.apiCountCompleteRateTooltip', tooltip: 'workbench.homePage.apiCountCompleteRateTooltip',
@ -109,8 +114,47 @@
}); });
const apiCountOptions = ref({}); const apiCountOptions = ref({});
const hasPermission = ref<boolean>(false); const hasPermission = ref<boolean>(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() { async function initApiCount() {
try { try {
const { startTime, endTime, dayNumber } = timeForm.value; const { startTime, endTime, dayNumber } = timeForm.value;
@ -127,18 +171,8 @@
const { statusStatisticsMap, statusPercentList, errorCode } = detail; const { statusStatisticsMap, statusPercentList, errorCode } = detail;
hasPermission.value = errorCode !== 109001; hasPermission.value = errorCode !== 109001;
apiCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList); 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( const { options: comOptions, valueList: completedList } = handleUpdateTabPie(
statusStatisticsMap?.completionRate || [], statusStatisticsMap?.completionRate || [],
@ -157,6 +191,7 @@
nextTick(() => { nextTick(() => {
initApiCount(); initApiCount();
emit('change'); emit('change');
initApiCountRate();
}); });
} }
@ -165,6 +200,26 @@
emit('change'); emit('change');
}); });
watch(
() => props.cover,
(val) => {
if (val) {
handleCoverData(val);
}
},
{
deep: true,
immediate: true,
}
);
watch(
() => props.status,
(val) => {
loading.value = val;
}
);
watch( watch(
() => innerProjectIds.value, () => innerProjectIds.value,
(val) => { (val) => {

View File

@ -21,7 +21,7 @@
</template> </template>
<div class="setting-wrapper"> <div class="setting-wrapper">
<div class="setting-wrapper-config"> <div class="setting-wrapper-config">
<CardSettingList @add="addCard" /> <CardSettingList v-model:selectedIds="selectedIds" @add="addCard" />
</div> </div>
<div class="setting-wrapper-content"> <div class="setting-wrapper-content">
<!-- 布局开始 --> <!-- 布局开始 -->
@ -131,8 +131,11 @@
const defaultAllProjectType = [WorkCardEnum.PROJECT_VIEW, WorkCardEnum.CREATE_BY_ME]; const defaultAllProjectType = [WorkCardEnum.PROJECT_VIEW, WorkCardEnum.CREATE_BY_ME];
const selectedIds = ref<WorkCardEnum[]>([]);
// //
function addCard(item: childrenWorkConfigItem) { function addCard(item: childrenWorkConfigItem) {
selectedIds.value.push(item.value);
const newCard: SelectedCardItem = { const newCard: SelectedCardItem = {
id: getGenerateId(), id: getGenerateId(),
fullScreen: !!isFullScreenType.includes(item.value), fullScreen: !!isFullScreenType.includes(item.value),
@ -153,6 +156,7 @@
function deleteHandler(item: SelectedCardItem) { function deleteHandler(item: SelectedCardItem) {
const index = selectedCardList.value.findIndex((e) => e.id === item.id); const index = selectedCardList.value.findIndex((e) => e.id === item.id);
selectedCardList.value.splice(index, 1); selectedCardList.value.splice(index, 1);
selectedIds.value.splice(index, 1);
} }
const confirmLoading = ref<boolean>(false); const confirmLoading = ref<boolean>(false);
@ -183,6 +187,7 @@
(val) => { (val) => {
if (val) { if (val) {
selectedCardList.value = cloneDeep(props.list); selectedCardList.value = cloneDeep(props.list);
selectedIds.value = props.list.map((e) => e.key);
} }
}, },
{ {

View File

@ -59,6 +59,10 @@
(e: 'add', item: childrenWorkConfigItem): void; (e: 'add', item: childrenWorkConfigItem): void;
}>(); }>();
const innerSelectedIds = defineModel<WorkCardEnum[]>('selectedIds', {
required: true,
});
const configList = ref<WorkConfigCard[]>([ const configList = ref<WorkConfigCard[]>([
// //
{ {
@ -212,17 +216,14 @@
const searchKeyword = ref(''); // const searchKeyword = ref(''); //
const filteredConfigList = computed(() => { const filteredConfigList = computed(() => {
if (!searchKeyword.value) { return configList.value.map((item) => ({
return configList.value; ...item,
} children: item.children.filter(
return configList.value (child) =>
.map((item) => ({ child.label.toLowerCase().includes(searchKeyword.value.toLowerCase()) &&
...item, !innerSelectedIds.value.includes(child.value)
children: item.children.filter((child) => ),
child.label.toLowerCase().includes(searchKeyword.value.toLowerCase()) }));
),
}))
.filter((item) => item.children.length > 0);
}); });
// //

View File

@ -46,7 +46,12 @@
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue'; 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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -93,12 +98,17 @@
const countOptions = ref<Record<string, any>>({}); const countOptions = ref<Record<string, any>>({});
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<PassRateDataType> = { const currentBugCount: (data: WorkHomePageDetail) => Promise<PassRateDataType> = {
[WorkCardEnum.BUG_COUNT]: workBugCountDetail, [WorkCardEnum.BUG_COUNT]: workBugCountDetail,
[WorkCardEnum.HANDLE_BUG_BY_ME]: workBugHandleByMe, [WorkCardEnum.HANDLE_BUG_BY_ME]: workBugHandleByMe,
[WorkCardEnum.CREATE_BUG_BY_ME]: workBugByMeCreated, [WorkCardEnum.CREATE_BUG_BY_ME]: workBugByMeCreated,
[WorkCardEnum.PLAN_LEGACY_BUG]: workPlanLegacyBug,
}[props.item.key as SelectedBugCountKeys]; }[props.item.key as SelectedBugCountKeys];
const hasPermission = ref<boolean>(false); const hasPermission = ref<boolean>(false);

View File

@ -27,7 +27,7 @@
</div> </div>
<!-- 概览图 --> <!-- 概览图 -->
<div> <div>
<MsChart height="300px" :options="options" /> <MsChart height="260px" :options="options" />
</div> </div>
</div> </div>
</template> </template>
@ -115,9 +115,11 @@
// x // x
options.value.xAxis.data = cardModuleList.value.map((e) => e.label); options.value.xAxis.data = cardModuleList.value.map((e) => e.label);
let maxAxis = 5;
// data // data
options.value.series = detail.projectCountList.map((item) => { options.value.series = detail.projectCountList.map((item) => {
const countData = item.count.map((e) => { const countData: Record<string, any> = item.count.map((e) => {
return { return {
name: item.name, name: item.name,
value: e !== 0 ? e : undefined, value: e !== 0 ? e : undefined,
@ -142,6 +144,11 @@
}, },
}; };
}); });
const itemMax = Math.max(...item.count);
maxAxis = Math.max(itemMax, maxAxis);
return { return {
name: item.name, name: item.name,
type: 'bar', type: 'bar',
@ -154,6 +161,10 @@
data: countData, 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() { async function initOverViewDetail() {
@ -178,6 +189,7 @@
handleData(detail); handleData(detail);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} }
} }

View File

@ -98,7 +98,11 @@
options.value.graphic.invisible = invisible; options.value.graphic.invisible = invisible;
options.value.graphic.style.text = text; options.value.graphic.style.text = text;
options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10)); options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10));
let maxAxis = 5;
options.value.series = detail.projectCountList.map((item, index) => { options.value.series = detail.projectCountList.map((item, index) => {
const itemMax = Math.max(...item.count);
maxAxis = Math.max(itemMax, maxAxis);
return { return {
name: t(contentTabList[index].label), name: t(contentTabList[index].label),
type: 'bar', type: 'bar',
@ -110,6 +114,7 @@
}, },
}; };
}); });
options.value.yAxis[0].max = maxAxis < 100 ? 50 : maxAxis + 50;
} }
async function initOverViewMemberDetail() { async function initOverViewMemberDetail() {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="pass-rate-content"> <a-spin class="pass-rate-content" :loading="props.loading">
<div class="relative flex items-center justify-center"> <div class="relative flex items-center justify-center">
<a-tooltip <a-tooltip
v-if="props.tooltipText" v-if="props.tooltipText"
@ -17,7 +17,7 @@
<div class="pass-rate-count">{{ hasPermission ? addCommasToNumber(item.value as number) : '-' }}</div> <div class="pass-rate-count">{{ hasPermission ? addCommasToNumber(item.value as number) : '-' }}</div>
</div> </div>
</div> </div>
</div> </a-spin>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -35,6 +35,7 @@
size: number; size: number;
tooltipText?: string; tooltipText?: string;
hasPermission: boolean; hasPermission: boolean;
loading?: boolean;
valueList: { valueList: {
label: string; label: string;
value: number | string; value: number | string;

View File

@ -1,17 +1,19 @@
<template> <template>
<div class="rate-content relative"> <a-spin class="w-full" :loading="props.loading">
<div class="relative flex h-full w-full items-center justify-center"> <div class="rate-content relative">
<a-tooltip <div class="relative flex h-full w-full items-center justify-center">
v-if="props.rateConfig.tooltipText" <a-tooltip
:mouse-enter-delay="500" v-if="props.rateConfig.tooltipText"
:content="t(props.rateConfig.tooltipText || '')" :mouse-enter-delay="500"
position="bottom" :content="t(props.rateConfig.tooltipText || '')"
> position="bottom"
<div class="tooltip-rate-tooltip"></div> >
</a-tooltip> <div class="tooltip-rate-tooltip"></div>
<MsChart :options="options" width="146px" /> </a-tooltip>
<MsChart :options="options" width="146px" />
</div>
</div> </div>
</div> </a-spin>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -28,6 +30,7 @@
const props = defineProps<{ const props = defineProps<{
data: { name: string; value: number }[]; data: { name: string; value: number }[];
hasPermission: boolean; hasPermission: boolean;
loading?: boolean;
rateConfig: { rateConfig: {
name: string; name: string;
color: string[]; color: string[];

View File

@ -94,6 +94,8 @@
v-model:projectIds="item.projectIds" v-model:projectIds="item.projectIds"
:type="item.key" :type="item.key"
:item="item" :item="item"
:status="projectLoadingStatus[item.projectIds[0]]"
:cover="requestResults.get(item.projectIds[0])"
@change="changeHandler" @change="changeHandler"
/> />
<ApiChangeList <ApiChangeList
@ -119,7 +121,9 @@
<ApiCount <ApiCount
v-else-if="item.key === WorkCardEnum.API_COUNT" v-else-if="item.key === WorkCardEnum.API_COUNT"
v-model:projectIds="item.projectIds" v-model:projectIds="item.projectIds"
:status="projectLoadingStatus[item.projectIds[0]]"
:item="item" :item="item"
:cover="requestResults.get(item.projectIds[0])"
@change="changeHandler" @change="changeHandler"
/> />
<TestPlanCount <TestPlanCount
@ -132,8 +136,8 @@
</div> </div>
<NoData <NoData
v-if="showNoData || !appStore.projectList.length" v-if="showNoData || !appStore.projectList.length"
:no-res-permission="!appStore.projectList.length" :no-res-permission="!defaultWorkList.length"
:all-screen="!!appStore.projectList.length" :all-screen="!!defaultWorkList.length"
height="h-[calc(100vh-110px)]" height="h-[calc(100vh-110px)]"
@config="cardSetting" @config="cardSetting"
/> />
@ -162,10 +166,11 @@
import DefectMemberBar from '@/views/workbench/homePage/components/defectMemberBar.vue'; import DefectMemberBar from '@/views/workbench/homePage/components/defectMemberBar.vue';
import OverviewMember from '@/views/workbench/homePage/components/overviewMember.vue'; import OverviewMember from '@/views/workbench/homePage/components/overviewMember.vue';
import { editDashboardLayout, getDashboardLayout } from '@/api/modules/workbench'; import { editDashboardLayout, getDashboardLayout, workApiCountCoverRage } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store'; import { useUserStore } from '@/store';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { sleep } from '@/utils';
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage'; import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
import { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage'; import { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
@ -260,8 +265,43 @@
} }
} }
onMounted(() => { // key ID
initDefaultList(); const requestResults = ref(new Map());
//
const requestedIds = ref(new Set());
const projectLoadingStatus = ref<Record<string, boolean>>({});
const fetchProjectDetails = async (projectId: string) => {
try {
projectLoadingStatus.value[projectId] = true;
const result = await workApiCountCoverRage(projectId);
requestResults.value.set(projectId, result);
projectLoadingStatus.value[projectId] = false;
await sleep(300);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
projectLoadingStatus.value[projectId] = false;
}
};
// id
async function requestQueue() {
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++) {
const item = queueList[i];
const [projectId] = item.projectIds;
if (!requestedIds.value.has(projectId)) {
requestedIds.value.add(projectId);
fetchProjectDetails(projectId);
}
}
}
onMounted(async () => {
const defaultTime = getLocalStorage(`WORK_TIME_${userStore.id}`); const defaultTime = getLocalStorage(`WORK_TIME_${userStore.id}`);
if (!defaultTime) { if (!defaultTime) {
setLocalStorage(`WORK_TIME_${userStore.id}`, JSON.stringify(timeForm.value)); setLocalStorage(`WORK_TIME_${userStore.id}`, JSON.stringify(timeForm.value));
@ -272,6 +312,8 @@
rangeTime.value = [startTime, endTime]; rangeTime.value = [startTime, endTime];
} }
} }
await initDefaultList();
requestQueue();
}); });
const time = ref({ ...timeForm.value }); const time = ref({ ...timeForm.value });

View File

@ -48,7 +48,7 @@ export const colorMapConfig: Record<string, string[]> = {
[WorkCardEnum.REVIEW_CASE_COUNT]: ['#9441B1', '#00C261', '#D4D4D8', '#3370FF'], [WorkCardEnum.REVIEW_CASE_COUNT]: ['#9441B1', '#00C261', '#D4D4D8', '#3370FF'],
[WorkCardEnum.TEST_PLAN_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'], [WorkCardEnum.TEST_PLAN_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.PLAN_LEGACY_BUG]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'], [WorkCardEnum.PLAN_LEGACY_BUG]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.BUG_COUNT]: ['#FFA200', '#00C261', '#D4D4D8'], [WorkCardEnum.BUG_COUNT]: ['#FFA200', '#D4D4D8', '#00C261'],
[WorkCardEnum.HANDLE_BUG_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'], [WorkCardEnum.HANDLE_BUG_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.CREATE_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'], [WorkCardEnum.CREATE_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.API_COUNT]: ['#811FA3', '#00C261', '#3370FF', '#FFA1FF', '#EE50A3', '#FF9964', '#F9F871', '#C3DD40'], [WorkCardEnum.API_COUNT]: ['#811FA3', '#00C261', '#3370FF', '#FFA1FF', '#EE50A3', '#FF9964', '#F9F871', '#C3DD40'],
@ -71,6 +71,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
enterable: true, enterable: true,
axisPointer: { axisPointer: {
type: 'shadow', type: 'shadow',
axis: 'auto',
}, },
formatter(params: any) { formatter(params: any) {
const html = ` const html = `
@ -81,7 +82,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
<div class="flex h-[18px] items-center justify-between"> <div class="flex h-[18px] items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm" style="background:${item.color}"></div> <div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm" style="background:${item.color}"></div>
<div class="one-line-text max-w-[120px]" style="color:#959598">${item.seriesName}</div> <div class="one-line-text max-w-[100px]" style="color:#959598">${item.seriesName}</div>
</div> </div>
<div class="text-[#323233] font-medium">${addCommasToNumber(item.value)}</div> <div class="text-[#323233] font-medium">${addCommasToNumber(item.value)}</div>
</div> </div>
@ -123,9 +124,14 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
}, },
yAxis: [ yAxis: [
{ {
type: 'log', type: 'value',
name: t('workbench.homePage.unit'), // 设置单位 name: t('workbench.homePage.unit'), // 设置单位
position: 'left', position: 'left',
axisLine: {
show: false,
onZero: false, // 禁止 Y 轴强制显示在 0 位置
},
offset: 0, // 第一个 Y 轴默认位置
nameTextStyle: { nameTextStyle: {
fontSize: 12, fontSize: 12,
color: '#AEAEB2', // 自定义字体大小和颜色 color: '#AEAEB2', // 自定义字体大小和颜色
@ -141,6 +147,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
}, },
}, },
min: 1, min: 1,
max: 0,
}, },
], ],
graphic: { graphic: {