mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-11-30 19:18:59 +08:00
feat(测试计划): 测试计划报告统计图详情页面和联调
This commit is contained in:
parent
f1421c81b0
commit
25f2f6a82b
@ -39,4 +39,9 @@ export function updateReportDetail(data: UpdateReportDetailParams) {
|
||||
return MSR.post({ url: reportUrl.UpdateReportDetailUrl, data });
|
||||
}
|
||||
|
||||
// 测试计划-报告-详情
|
||||
export function getReportDetail(id: string) {
|
||||
return MSR.get({ url: `${reportUrl.PlanReportDetailUrl}/${id}` });
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
@ -6,6 +6,8 @@ export const PlanReportRenameUrl = '/test-plan/report/rename';
|
||||
export const PlanDeleteUrl = '/test-plan/report/delete';
|
||||
// 批量删除报告
|
||||
export const PlanBatchDeleteUrl = '/test-plan/report/batch-delete';
|
||||
// 测试计划-报告-详情
|
||||
export const PlanReportDetailUrl = '/test-plan/report/get';
|
||||
// 测试计划-报告-详情-缺陷分页查询
|
||||
export const ReportBugListUrl = '/test-plan/report/detail/bug/page';
|
||||
// 测试计划-报告-详情-功能用例分页查询
|
||||
|
4
frontend/src/assets/svg/bugTotal.svg
Normal file
4
frontend/src/assets/svg/bugTotal.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="4" fill="#F02929"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.33261 9.33317H8.00008L8.00008 12.9998L8.00008 13.3332C8.00008 13.8183 8.14793 14.848 8.72526 15.7338C9.20136 16.4643 9.9862 17.129 11.3334 17.2941V9.33317L9.33341 9.33317C9.33315 9.33317 9.33288 9.33317 9.33261 9.33317ZM12.6667 9.33317V17.2944C14.0157 17.1306 14.8002 16.4699 15.2755 15.7432C15.8518 14.8619 16.0001 13.833 16.0001 13.3332V12.9998V9.33317H14.6676C14.6673 9.33317 14.667 9.33317 14.6667 9.33317L12.6667 9.33317ZM15.3317 7.99984H16.3906L18.1953 6.1951C18.4557 5.93475 18.8778 5.93475 19.1382 6.1951C19.3985 6.45545 19.3985 6.87756 19.1382 7.13791L17.3334 8.94265V12.3332H18.6667C19.0349 12.3332 19.3334 12.6316 19.3334 12.9998C19.3334 13.368 19.0349 13.6665 18.6667 13.6665H17.3205C17.2677 14.3838 17.0405 15.4728 16.4011 16.458L18.1382 18.1951C18.3985 18.4554 18.3985 18.8776 18.1382 19.1379C17.8778 19.3983 17.4557 19.3983 17.1953 19.1379L15.5267 17.4693C14.7128 18.1766 13.5689 18.6665 12.0001 18.6665C10.4328 18.6665 9.29011 18.1742 8.47693 17.4658L6.80482 19.1379C6.54447 19.3983 6.12236 19.3983 5.86201 19.1379C5.60166 18.8776 5.60166 18.4554 5.86201 18.1951L7.60312 16.454C6.96249 15.4671 6.73371 14.3777 6.68008 13.6665H5.33341C4.96522 13.6665 4.66675 13.368 4.66675 12.9998C4.66675 12.6316 4.96522 12.3332 5.33341 12.3332H6.66675V8.94265L4.86201 7.13791C4.60166 6.87756 4.60166 6.45545 4.86201 6.1951C5.12236 5.93475 5.54447 5.93475 5.80482 6.1951L7.60956 7.99984H8.66847C8.725 6.17306 10.1707 4.6665 12.0001 4.6665C13.8295 4.6665 15.2752 6.17306 15.3317 7.99984ZM13.9974 7.99984C13.9423 6.86245 13.0455 5.99984 12.0001 5.99984C10.9547 5.99984 10.0579 6.86245 10.0028 7.99984L12.0001 7.99984L13.9974 7.99984Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
4
frontend/src/assets/svg/passRate.svg
Normal file
4
frontend/src/assets/svg/passRate.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="4" fill="#00C261"/>
|
||||
<path d="M12.0001 17.9998C15.3138 17.9998 18.0001 15.3135 18.0001 11.9998C18.0001 8.68613 15.3138 5.99984 12.0001 5.99984C8.68637 5.99984 6.00008 8.68613 6.00008 11.9998C6.00008 15.3135 8.68637 17.9998 12.0001 17.9998ZM12.0001 19.3332C7.94999 19.3332 4.66675 16.0499 4.66675 11.9998C4.66675 7.94975 7.94999 4.6665 12.0001 4.6665C16.0502 4.6665 19.3334 7.94975 19.3334 11.9998C19.3334 16.0499 16.0502 19.3332 12.0001 19.3332ZM11.2267 13.5198L14.7623 9.98427C14.8925 9.85409 15.1035 9.85409 15.2337 9.98427L15.7051 10.4557C15.8353 10.5858 15.8353 10.7969 15.7051 10.9271L11.4625 15.1697C11.3323 15.2999 11.1212 15.2999 10.991 15.1697L8.55077 12.7294C8.42059 12.5993 8.42059 12.3882 8.55077 12.258L9.02217 11.7866C9.15235 11.6565 9.3634 11.6565 9.49358 11.7866L11.2267 13.5198Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 947 B |
5
frontend/src/assets/svg/threshold.svg
Normal file
5
frontend/src/assets/svg/threshold.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="4" fill="#00C261"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.66675 11.9998C4.66675 7.94975 7.94999 4.6665 12.0001 4.6665C13.3382 4.6665 14.5946 5.02553 15.6758 5.65296C15.9943 5.83775 16.1026 6.24572 15.9178 6.56417C15.733 6.88263 15.3251 6.99099 15.0066 6.80619C14.1232 6.29356 13.0969 5.99984 12.0001 5.99984C8.68637 5.99984 6.00008 8.68613 6.00008 11.9998C6.00008 15.3135 8.68637 17.9998 12.0001 17.9998C15.3138 17.9998 18.0001 15.3135 18.0001 11.9998C18.0001 10.9105 17.7104 9.8909 17.2043 9.01164C17.0207 8.69253 17.1304 8.28495 17.4495 8.10128C17.7687 7.91761 18.1762 8.0274 18.3599 8.3465C18.9794 9.42271 19.3334 10.6709 19.3334 11.9998C19.3334 16.0499 16.0502 19.3332 12.0001 19.3332C7.94999 19.3332 4.66675 16.0499 4.66675 11.9998Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2743 9.58379L14.65 9.34995L14.4162 8.72564C14.6609 8.63398 14.9366 8.69376 15.1214 8.87855C15.3062 9.06334 15.366 9.33906 15.2743 9.58379ZM13.4568 10.5432C13.4174 10.5597 13.3777 10.5765 13.3379 10.5934C12.9342 10.7646 12.5235 10.9482 12.1863 11.1179C12.0174 11.2029 11.8743 11.2807 11.7631 11.3485C11.7078 11.3823 11.6645 11.4111 11.6322 11.4346C11.6 11.4581 11.5859 11.4712 11.5859 11.4712C11.3256 11.7316 11.3255 12.1537 11.5859 12.4141C11.8462 12.6744 12.2683 12.6744 12.5287 12.4141C12.5287 12.4141 12.5419 12.4 12.5653 12.3678C12.5889 12.3355 12.6176 12.2922 12.6514 12.2368C12.7193 12.1256 12.7971 11.9826 12.8821 11.8137C13.0518 11.4764 13.2353 11.0657 13.4066 10.6621C13.4235 10.6223 13.4402 10.5826 13.4568 10.5432ZM14.65 9.34995C14.4162 8.72564 14.4162 8.72564 14.4162 8.72564L14.4066 8.72925L14.3802 8.73919C14.3573 8.74784 14.324 8.76051 14.2815 8.77675C14.1965 8.80923 14.0749 8.85607 13.9275 8.91386C13.6331 9.02928 13.2339 9.1891 12.8171 9.36593C12.402 9.54205 11.9614 9.73843 11.5869 9.92688C11.3999 10.021 11.222 10.1168 11.0686 10.2104C10.9246 10.2983 10.7656 10.4059 10.6431 10.5284C9.86203 11.3095 9.86203 12.5758 10.6431 13.3569C11.4241 14.1379 12.6905 14.1379 13.4715 13.3569C13.594 13.2344 13.7017 13.0754 13.7896 12.9313C13.8832 12.778 13.979 12.6001 14.0731 12.4131C14.2615 12.0386 14.4579 11.598 14.634 11.1828C14.8109 10.7661 14.9707 10.3669 15.0861 10.0725C15.1439 9.9251 15.1907 9.8035 15.2232 9.71851C15.2395 9.67601 15.2521 9.64263 15.2608 9.61974L15.2707 9.59341L15.2743 9.58379C15.2743 9.58379 15.2743 9.58379 14.65 9.34995Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
@ -40,7 +40,7 @@
|
||||
label: 'BLOCKED',
|
||||
icon: StatusType.BLOCKED,
|
||||
statusText: t('caseManagement.featureCase.chokeUp'),
|
||||
color: 'rgb(var(--warning-6))',
|
||||
color: 'rgb(var(--primary-3))',
|
||||
},
|
||||
ERROR: {
|
||||
label: 'ERROR',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { PassRateCountDetail, planStatusType, TestPlanDetail } from '@/models/testPlan/testPlan';
|
||||
import type { PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
|
||||
import { LastExecuteResults } from '@/enums/caseEnum';
|
||||
|
||||
// TODO: 对照后端字段
|
||||
// 测试计划详情
|
||||
export const testPlanDefaultDetail: TestPlanDetail = {
|
||||
@ -46,5 +46,77 @@ export const defaultExecuteForm = {
|
||||
planCommentFileIds: [],
|
||||
notifier: [] as string[],
|
||||
};
|
||||
// 报告详情
|
||||
export const defaultReportDetail: PlanReportDetail = {
|
||||
id: '',
|
||||
name: '',
|
||||
startTime: 0,
|
||||
executeTime: 0, // 报告执行开始时间
|
||||
endTime: 0,
|
||||
summary: '',
|
||||
passThreshold: 0, // 通过阈值
|
||||
passRate: 0, // 通过率
|
||||
executeRate: 0, // 执行完成率
|
||||
bugCount: 0,
|
||||
caseTotal: 0,
|
||||
executeCount: {
|
||||
success: 0,
|
||||
error: 0,
|
||||
fakeError: 0,
|
||||
block: 0,
|
||||
pending: 0,
|
||||
},
|
||||
functionalCount: {
|
||||
success: 0,
|
||||
error: 0,
|
||||
fakeError: 0,
|
||||
block: 0,
|
||||
pending: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const statusConfig: StatusListType[] = [
|
||||
{
|
||||
label: 'common.success',
|
||||
value: 'success',
|
||||
color: '#00C261',
|
||||
class: 'bg-[rgb(var(--success-6))]',
|
||||
rateKey: 'requestPassRate',
|
||||
key: 'SUCCESS',
|
||||
},
|
||||
// TODO 这个版本不展示误报
|
||||
// {
|
||||
// label: 'common.fakeError',
|
||||
// value: 'fakeError',
|
||||
// color: '#FFC14E',
|
||||
// class: 'bg-[rgb(var(--warning-6))]',
|
||||
// rateKey: 'requestFakeErrorRate',
|
||||
// key: 'FAKE_ERROR',
|
||||
// },
|
||||
{
|
||||
label: 'common.fail',
|
||||
value: 'error',
|
||||
color: '#ED0303',
|
||||
class: 'bg-[rgb(var(--danger-6))]',
|
||||
rateKey: 'requestErrorRate',
|
||||
key: 'ERROR',
|
||||
},
|
||||
{
|
||||
label: 'common.unExecute',
|
||||
value: 'pending',
|
||||
color: '#D4D4D8',
|
||||
class: 'bg-[var(--color-text-input-border)]',
|
||||
rateKey: 'requestPendingRate',
|
||||
key: 'PENDING',
|
||||
},
|
||||
{
|
||||
label: 'common.block',
|
||||
value: 'block',
|
||||
color: '#B379C8',
|
||||
class: 'bg-[rgb(var(--primary-3))]',
|
||||
rateKey: 'requestPendingRate',
|
||||
key: 'BLOCK',
|
||||
},
|
||||
];
|
||||
|
||||
export default {};
|
||||
|
34
frontend/src/models/testPlan/testPlanReport.ts
Normal file
34
frontend/src/models/testPlan/testPlanReport.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export interface countDetail {
|
||||
success: number;
|
||||
error: number;
|
||||
fakeError: number;
|
||||
block: number;
|
||||
pending: number;
|
||||
}
|
||||
export interface PlanReportDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
startTime: number;
|
||||
executeTime: number; // 报告执行开始时间
|
||||
endTime: number;
|
||||
summary: string;
|
||||
passThreshold: number; // 通过阈值
|
||||
passRate: number; // 通过率
|
||||
executeRate: number; // 执行完成率
|
||||
bugCount: number;
|
||||
caseTotal: number;
|
||||
executeCount: countDetail;
|
||||
functionalCount: countDetail;
|
||||
// TOTO 这个版本不展示场景和接口
|
||||
// apiCaseCount: countDetail; // 接口场景用例分析-用例数
|
||||
// apiScenarioCount: countDetail; // 接口场景用例分析-用例数
|
||||
}
|
||||
|
||||
export interface StatusListType {
|
||||
label: string;
|
||||
value: keyof countDetail;
|
||||
color: string;
|
||||
class: string;
|
||||
rateKey: string;
|
||||
key: string;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div class="flex min-h-[110px] items-center justify-between">
|
||||
<div class="relative mr-4">
|
||||
<div class="absolute bottom-0 left-[30%] top-[35%] text-center">
|
||||
<div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div>
|
||||
<div class="text-[18px] font-medium">1</div>
|
||||
</div>
|
||||
<MsChart width="110px" height="110px" :options="props.options" />
|
||||
</div>
|
||||
<div class="chart-legend grid flex-1 gap-y-3">
|
||||
<!-- 图例开始 -->
|
||||
<div v-for="item of props.legendData" :key="item.value" class="chart-legend-item">
|
||||
<div class="chart-flag">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full" :class="item.class"></div>
|
||||
<div class="mr-2 text-[var(--color-text-4)]">{{ item.label }}</div>
|
||||
</div>
|
||||
<div class="count">{{ item.count || 0 }}</div>
|
||||
<div class="count">{{ item.rote || 0 }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 用例报告独立报告
|
||||
*/
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
options: Record<string, any>;
|
||||
legendData: LegendData[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chart-legend {
|
||||
.chart-legend-item {
|
||||
@apply grid grid-cols-3 gap-2;
|
||||
}
|
||||
.chart-flag {
|
||||
@apply flex items-center;
|
||||
.count {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex min-h-[110px] items-center">
|
||||
<div :class="`min-h-[${props.size || '110px'}] flex items-center`">
|
||||
<div class="relative mr-4">
|
||||
<div class="charts absolute text-center">
|
||||
<div :class="`${props.offset || defaultOffset} charts absolute text-center`">
|
||||
<div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div>
|
||||
<a-popover position="bottom" content-class="response-popover-content">
|
||||
<div class="flex justify-center text-[18px] font-medium">
|
||||
@ -18,7 +18,7 @@
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-popover position="bottom" content-class="response-popover-content">
|
||||
<div> <MsChart width="110px" height="110px" :options="props.options" /></div>
|
||||
<div> <MsChart :width="props.size || '110px'" :height="props.size || '110px'" :options="props.options" /></div>
|
||||
<template #content>
|
||||
<div class="min-w-[176px] max-w-[400px] p-4">
|
||||
<div v-for="item of legendData" :key="item.value" class="mb-2 flex justify-between">
|
||||
@ -40,8 +40,10 @@
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full" :class="item.class"></div>
|
||||
<div class="mr-2 text-[var(--color-text-4)]">{{ item.label }}</div>
|
||||
</div>
|
||||
<div class="count">{{ item.count || 0 }}</div>
|
||||
<div class="count">{{ item.rote || 0 }} <span v-if="String(item.rote) !== 'Calculating'">%</span></div>
|
||||
<div class="count font-medium">{{ item.count || 0 }}</div>
|
||||
<div class="count text-right font-medium"
|
||||
>{{ item.rote || 0 }} <span v-if="String(item.rote) !== 'Calculating'"></span
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,7 +58,7 @@
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber, formatDuration } from '@/utils';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
|
||||
@ -67,7 +69,11 @@
|
||||
options: Record<string, any>;
|
||||
legendData: LegendData[];
|
||||
requestTotal: number;
|
||||
size?: string;
|
||||
offset?: string;
|
||||
}>();
|
||||
|
||||
const defaultOffset = ref('top-[30%] right-0 bottom-0 left-0');
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@ -83,10 +89,6 @@
|
||||
}
|
||||
}
|
||||
.charts {
|
||||
top: 30%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
margin: auto;
|
||||
}
|
||||
|
@ -383,7 +383,7 @@
|
||||
...item,
|
||||
label: t(item.label),
|
||||
count: detail.value[item.value] || 0,
|
||||
rote: detail.value[item.rateKey] || 0,
|
||||
rote: `${detail.value[item.rateKey] || 0}%`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -360,7 +360,7 @@
|
||||
...item,
|
||||
label: t(item.label),
|
||||
count: detail.value[item.value] || 0,
|
||||
rote: detail.value[item.rateKey] || 0,
|
||||
rote: `${detail.value[item.rateKey] || 0}%`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -30,7 +30,7 @@
|
||||
{{ t('caseManagement.featureCase.execute.success') }}
|
||||
</div>
|
||||
<div v-if="item.status === 'BLOCKED'" class="flex items-center">
|
||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--primary-3))]" />
|
||||
{{ t('caseManagement.featureCase.execute.blocked') }}
|
||||
</div>
|
||||
<div v-if="item.status === 'ERROR'" class="flex items-center">
|
||||
|
@ -72,7 +72,7 @@ export const executionResultMap: Record<string, any> = {
|
||||
key: 'BLOCKED',
|
||||
icon: StatusType.BLOCKED,
|
||||
statusText: t('caseManagement.featureCase.chokeUp'),
|
||||
color: 'text-[rgb(var(--warning-6))]',
|
||||
color: 'text-[rgb(var(--primary-3))]',
|
||||
},
|
||||
ERROR: {
|
||||
key: 'ERROR',
|
||||
|
@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="mb-[16px]">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
><span class="text-[var(--color-text-4)]">{{ t(statusObject.label) }}</span
|
||||
><span class="ml-2 font-medium text-[var(--color-text-1)]">{{ countDetailData[props.status] }}</span></div
|
||||
>
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ getPassRate }}</div>
|
||||
</div>
|
||||
|
||||
<MsColorLine :color-data="colorData" height="8px" radius="2px">
|
||||
<template #popoverContent>
|
||||
<table class="min-w-[230px]">
|
||||
<tr class="flex items-center">
|
||||
<td class="w-[50%]">
|
||||
<div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div>
|
||||
</td>
|
||||
<td class="-ml-[2px] font-medium">
|
||||
{{ props.detail.caseTotal || 0 }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="popover-tr">
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-input-border)]"></div>
|
||||
<div>{{ t('common.unExecute') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td-count">
|
||||
{{ addCommasToNumber(countDetailData.pending) }}
|
||||
</td>
|
||||
<td class="popover-value-td-pass">
|
||||
{{ statusExecuteRate.pendingRateResult }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="popover-tr">
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
|
||||
<div>{{ t('common.success') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td-count">
|
||||
{{ addCommasToNumber(countDetailData.success) }}
|
||||
</td>
|
||||
<td class="popover-value-td-pass">
|
||||
{{ statusExecuteRate.successRateResult }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- TODO 没有接口暂时不上 -->
|
||||
<!-- <tr>
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
|
||||
<div>{{ t('common.fakeError') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ detailCount.fakeErrorCount }}
|
||||
</td>
|
||||
</tr> -->
|
||||
<tr class="popover-tr">
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--primary-3))]"></div>
|
||||
<div>{{ t('common.block') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td-count">
|
||||
{{ addCommasToNumber(countDetailData.block) }}
|
||||
</td>
|
||||
<td class="popover-value-td-pass">
|
||||
{{ statusExecuteRate.blockRateResult }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="popover-tr">
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
|
||||
<div>{{ t('common.fail') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td-count">
|
||||
{{ addCommasToNumber(countDetailData.error) }}
|
||||
</td>
|
||||
<td class="popover-value-td-pass">
|
||||
{{ statusExecuteRate.errorRateResult }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</MsColorLine>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import MsColorLine from '@/components/pure/ms-color-line/index.vue';
|
||||
|
||||
import { statusConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { countDetail, PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
detail: PlanReportDetail;
|
||||
status: keyof countDetail;
|
||||
}>();
|
||||
|
||||
const defaultStatus = {
|
||||
label: 'common.unExecute',
|
||||
value: 'pending',
|
||||
color: '#D4D4D8',
|
||||
class: 'bg-[var(--color-text-input-border)]',
|
||||
rateKey: 'requestPendingRate',
|
||||
key: 'PENDING',
|
||||
};
|
||||
|
||||
const statusObject = computed(() => {
|
||||
return statusConfig.find((e) => e.value === props.status) || defaultStatus;
|
||||
});
|
||||
|
||||
const countDetailData = computed(() => {
|
||||
return props.detail.functionalCount;
|
||||
});
|
||||
|
||||
const colorData = computed(() => {
|
||||
if (countDetailData.value[props.status] === 0) {
|
||||
return [
|
||||
{
|
||||
percentage: 100,
|
||||
color: 'var(--color-text-n8)',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
percentage: (countDetailData.value[props.status] / props.detail.caseTotal) * 100,
|
||||
color: statusObject.value.color,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const getPassRate = computed(() => {
|
||||
const result = (countDetailData.value[props.status] / props.detail.caseTotal) * 100;
|
||||
return `${Number.isNaN(result) ? 0 : result.toFixed(2)}%`;
|
||||
});
|
||||
|
||||
const calculateRate = (count: number) => {
|
||||
const rate = (count / props.detail.caseTotal) * 100;
|
||||
return `${Number.isNaN(rate) ? 0 : rate.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const statusExecuteRate = computed(() => {
|
||||
const pendingRateResult = calculateRate(countDetailData.value.pending);
|
||||
const successRateResult = calculateRate(countDetailData.value.success);
|
||||
const errorRateResult = calculateRate(countDetailData.value.error);
|
||||
const blockRateResult = calculateRate(countDetailData.value.block);
|
||||
|
||||
return {
|
||||
pendingRateResult,
|
||||
successRateResult,
|
||||
errorRateResult,
|
||||
blockRateResult,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.popover-tr {
|
||||
@apply flex items-center justify-between;
|
||||
.popover-label-td {
|
||||
padding: 8px 8px 0 0;
|
||||
width: 220px;
|
||||
color: var(--color-text-4);
|
||||
@apply flex flex-1 items-center;
|
||||
}
|
||||
.popover-value-td-count {
|
||||
@apply flex-1 text-center font-medium;
|
||||
|
||||
padding-top: 8px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.popover-value-td-pass {
|
||||
@apply flex-1 text-right font-medium;
|
||||
|
||||
padding-top: 8px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,5 +1,136 @@
|
||||
<template>
|
||||
<MsCard class="mb-[16px]" hide-back hide-footer auto-height no-content-padding hide-divider> </MsCard>
|
||||
<MsCard class="mb-[16px]" hide-back hide-footer auto-height no-content-padding hide-divider>
|
||||
<template #headerLeft>
|
||||
<div v-if="route.query.id" class="flex items-center font-medium"
|
||||
>{{ t('report.name') }}
|
||||
<a-tooltip :content="detail.name" :mouse-enter-delay="300"
|
||||
><div class="one-line-text max-w-[300px]">{{ detail.name }}</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #headerRight>
|
||||
<a-popover position="bottom" content-class="response-popover-content">
|
||||
<div>
|
||||
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
|
||||
{{ detail.executeTime ? dayjs(detail.executeTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
|
||||
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
|
||||
{{ detail.endTime ? dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="max-w-[400px] items-center gap-[8px] text-[14px]">
|
||||
<div class="flex-shrink-0 text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</div>
|
||||
<div class="mt-2">
|
||||
{{ dayjs(detail.executeTime).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="ml-4 !rounded-[var(--border-radius-small)]"
|
||||
:loading="shareLoading"
|
||||
@click="shareHandler"
|
||||
>
|
||||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
||||
{{ t('common.share') }}
|
||||
</MsButton>
|
||||
</template>
|
||||
</MsCard>
|
||||
<div class="analysis-wrapper">
|
||||
<div class="analysis">
|
||||
<div>
|
||||
<div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div>
|
||||
<ul class="report-analysis">
|
||||
<li class="report-analysis-item">
|
||||
<div class="report-analysis-item-icon">
|
||||
<svg-icon class="mr-2" width="24px" height="24px" name="threshold" />
|
||||
<span>{{ t('report.detail.threshold') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="report-analysis-item-number">{{ detail.passThreshold }}</span>
|
||||
<span class="report-analysis-item-unit">(%)</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="report-analysis-item">
|
||||
<div class="report-analysis-item-icon">
|
||||
<svg-icon class="mr-2" width="24px" height="24px" name="passRate" />
|
||||
<span>{{ t('report.detail.reportPassRate') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="report-analysis-item-number">{{ detail.passRate }}</span>
|
||||
<span class="report-analysis-item-unit"> (%)</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="report-analysis-item">
|
||||
<div class="report-analysis-item-icon">
|
||||
<svg-icon class="mr-2" width="24px" height="24px" name="passRate" />
|
||||
<span>{{ t('report.detail.performCompletion') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="report-analysis-item-number">{{ detail.executeRate }}</span>
|
||||
<span class="report-analysis-item-unit">(%)</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="report-analysis-item">
|
||||
<div class="report-analysis-item-icon">
|
||||
<svg-icon class="mr-2" width="24px" height="24px" name="bugTotal" />
|
||||
<span>{{ t('report.detail.totalDefects') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="report-analysis-item-number">{{ addCommasToNumber(detail.bugCount) }}</span>
|
||||
<span class="report-analysis-item-unit">({{ t('report.detail.number') }})</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis mx-4">
|
||||
<div>
|
||||
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
|
||||
<SetReportChart
|
||||
size="160px"
|
||||
offset="top-[34%] right-0 bottom-0 left-0"
|
||||
:legend-data="legendData"
|
||||
:options="charOptions"
|
||||
:request-total="getIndicators(detail.caseTotal) || 0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis">
|
||||
<div>
|
||||
<div class="block-title">{{ t('report.detail.useCaseAnalysis') }}</div>
|
||||
<div class="flex">
|
||||
<div class="w-[70%]">
|
||||
<SingleStatusProgress :detail="detail" status="pending" />
|
||||
<SingleStatusProgress :detail="detail" status="success" />
|
||||
<SingleStatusProgress :detail="detail" status="block" />
|
||||
<SingleStatusProgress :detail="detail" status="error" />
|
||||
</div>
|
||||
<div class="relative w-[30%]">
|
||||
<div class="charts absolute w-full text-center">
|
||||
<div class="text-[12px] !text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
|
||||
<a-popover position="bottom" content-class="response-popover-content">
|
||||
<div class="flex justify-center text-[18px] font-medium">
|
||||
<div class="one-line-text max-w-[60px] text-[var(--color-text-1)]">{{ detail.passRate }}% </div>
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="min-w-[95px] max-w-[400px] p-4 text-[14px]">
|
||||
<div class="text-[12px] font-medium text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
|
||||
<div class="mt-2 text-[18px] font-medium text-[var(--color-text-1)]">{{ detail.passRate }} %</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<MsChart width="120px" height="120px" :options="functionCaseOptions"
|
||||
/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MsCard class="mb-[16px]" simple auto-height>
|
||||
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
|
||||
<MsRichText
|
||||
@ -30,21 +161,197 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||
import SingleStatusProgress from '../component/singleStatusProgress.vue';
|
||||
import BugTable from './component/bugTable.vue';
|
||||
import FeatureCaseTable from './component/featureCaseTable.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
|
||||
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
|
||||
import { getReportDetail } from '@/api/modules/test-plan/report';
|
||||
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
|
||||
import { defaultReportDetail, statusConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type { PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
import { getIndicators } from '@/views/api-test/report/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const reportId = ref(route.query.id as string);
|
||||
const reportId = ref<string>(route.query.id as string);
|
||||
|
||||
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
|
||||
|
||||
// 分享
|
||||
const shareLoading = ref<boolean>(false);
|
||||
|
||||
function shareHandler() {}
|
||||
|
||||
const legendData = ref<LegendData[]>([]);
|
||||
|
||||
const charOptions = ref({
|
||||
tooltip: {
|
||||
show: false,
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['65%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.success'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fakeError'),
|
||||
itemStyle: {
|
||||
color: '#FFC14E',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fail'),
|
||||
itemStyle: {
|
||||
color: '#ED0303',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.unExecute'),
|
||||
itemStyle: {
|
||||
color: '#D4D4D8',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.block'),
|
||||
itemStyle: {
|
||||
color: '#B379C8',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const functionCaseOptions = ref({
|
||||
tooltip: {
|
||||
show: false,
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['65%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.success'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化图表
|
||||
function initOptionsData() {
|
||||
charOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
||||
return {
|
||||
value: detail.value.executeCount[item.value] || 0,
|
||||
name: t(item.label),
|
||||
itemStyle: {
|
||||
color: item.color,
|
||||
},
|
||||
};
|
||||
});
|
||||
legendData.value = statusConfig.map((item: StatusListType) => {
|
||||
const rate = (detail.value.executeCount[item.value] / detail.value.caseTotal) * 100;
|
||||
return {
|
||||
...item,
|
||||
label: t(item.label),
|
||||
count: detail.value.executeCount[item.value] || 0,
|
||||
rote: `${Number.isNaN(rate) ? 0 : rate.toFixed(2)}%`,
|
||||
};
|
||||
}) as unknown as LegendData[];
|
||||
|
||||
functionCaseOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
||||
return {
|
||||
value: detail.value.functionalCount[item.value] || 0,
|
||||
name: t(item.label),
|
||||
itemStyle: {
|
||||
color: item.color,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getDetail() {
|
||||
try {
|
||||
detail.value = await getReportDetail(reportId.value);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getDetail();
|
||||
initOptionsData();
|
||||
});
|
||||
|
||||
const activeTab = ref('bug');
|
||||
const contentTabList = ref([
|
||||
{
|
||||
@ -76,4 +383,43 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped lang="less">
|
||||
.block-title {
|
||||
@apply mb-4 font-medium;
|
||||
}
|
||||
.analysis-wrapper {
|
||||
height: 250px;
|
||||
@apply mb-4 flex items-center;
|
||||
.analysis {
|
||||
padding: 24px;
|
||||
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
|
||||
@apply h-full flex-1 rounded-xl bg-white;
|
||||
.report-analysis {
|
||||
.report-analysis-item {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-text-n9);
|
||||
@apply mb-3 flex items-center justify-between;
|
||||
.report-analysis-item-icon {
|
||||
@apply flex items-center;
|
||||
}
|
||||
.report-analysis-item-number {
|
||||
@apply font-medium;
|
||||
}
|
||||
.report-analysis-item-unit {
|
||||
color: var(--color-text-4);
|
||||
@apply ml-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.charts {
|
||||
top: 34%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -36,4 +36,11 @@ export default {
|
||||
'report.detail.reportSummary': 'Report summary',
|
||||
'report.detail.bugDetails': 'Bug details',
|
||||
'report.detail.featureCaseDetails': 'Feature case details',
|
||||
'report.detail.executionAnalysis': 'Execution Analysis',
|
||||
'report.detail.threshold': 'Pass threshold',
|
||||
'report.detail.reportPassRate': 'The report pass',
|
||||
'report.detail.performCompletion': 'Perform completion',
|
||||
'report.detail.totalDefects': 'Total defects',
|
||||
'report.detail.useCaseAnalysis': 'Function of use case analysis',
|
||||
'report.detail.number': 'number',
|
||||
};
|
||||
|
@ -36,4 +36,11 @@ export default {
|
||||
'report.detail.reportSummary': '报告总结',
|
||||
'report.detail.bugDetails': '缺陷明细',
|
||||
'report.detail.featureCaseDetails': '功能用例明细',
|
||||
'report.detail.executionAnalysis': '执行分析',
|
||||
'report.detail.threshold': '通过阈值',
|
||||
'report.detail.reportPassRate': '报告通过率',
|
||||
'report.detail.performCompletion': '执行完成率',
|
||||
'report.detail.totalDefects': '缺陷总数',
|
||||
'report.detail.useCaseAnalysis': '功能用例分析',
|
||||
'report.detail.number': '个',
|
||||
};
|
||||
|
@ -32,7 +32,8 @@
|
||||
{{ detailCount.errorCount }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- TODO 这个版本暂时不上 -->
|
||||
<!-- <tr>
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
|
||||
<div>{{ t('common.fakeError') }}</div>
|
||||
@ -40,10 +41,10 @@
|
||||
<td class="popover-value-td">
|
||||
{{ detailCount.fakeErrorCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</tr> -->
|
||||
<tr>
|
||||
<td class="popover-label-td">
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--link-6))]"></div>
|
||||
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--primary-3))]"></div>
|
||||
<div>{{ t('common.block') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
|
@ -14,7 +14,7 @@
|
||||
{{ t('common.success') }}
|
||||
</div>
|
||||
<div v-if="item.status === 'BLOCKED'" class="flex items-center">
|
||||
<MsIcon type="icon-icon_block_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||
<MsIcon type="icon-icon_block_filled" class="mr-[4px] text-[rgb(var(--primary-3))]" />
|
||||
{{ t('common.block') }}
|
||||
</div>
|
||||
<div v-if="item.status === 'ERROR'" class="flex items-center">
|
||||
|
@ -397,12 +397,6 @@
|
||||
activeId.value = item.id;
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => activeId.value,
|
||||
() => {
|
||||
loadCaseDetail();
|
||||
}
|
||||
);
|
||||
|
||||
async function loadCase() {
|
||||
await loadCaseList();
|
||||
@ -481,7 +475,7 @@
|
||||
async function getBugTotal() {
|
||||
try {
|
||||
const params = {
|
||||
testPlanCaseId: route.query.testPlanCaseId,
|
||||
testPlanCaseId: activeId.value,
|
||||
caseId: activeCaseId.value,
|
||||
projectId: appStore.currentProjectId,
|
||||
current: 1,
|
||||
@ -562,6 +556,14 @@
|
||||
initBugList();
|
||||
await loadCase();
|
||||
});
|
||||
watch(
|
||||
() => activeId.value,
|
||||
() => {
|
||||
loadCaseDetail();
|
||||
initBugList();
|
||||
getBugTotal();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => activeTab.value,
|
||||
|
Loading…
Reference in New Issue
Block a user