feat(测试计划): 测试计划组联调批量开启关闭定时任务&部分集成报告页面&拆分报告总结和报告头

This commit is contained in:
xinxin.wu 2024-06-07 21:38:30 +08:00 committed by Craftsman
parent 5ad2775eb8
commit 4d0cbcbf7a
18 changed files with 621 additions and 113 deletions

View 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="#9441B1"/>
<path d="M12.6667 4.66666L12.7207 4.66866L12.7661 4.674C12.7774 4.67574 12.789 4.67779 12.8006 4.68015C12.8152 4.68308 12.8297 4.68655 12.844 4.69047C12.8544 4.69339 12.8646 4.69648 12.8748 4.69982C12.8879 4.70407 12.9011 4.70887 12.9141 4.71406L12.9489 4.72911C12.9627 4.73557 12.9763 4.74252 12.9896 4.74991C12.9977 4.75432 13.0058 4.75906 13.0139 4.76399C13.0309 4.77447 13.0475 4.78565 13.0635 4.79753L13.0781 4.80861C13.0799 4.81004 13.0818 4.81157 13.0837 4.81312L13.1382 4.86192L16.4715 8.19525L16.5167 8.24666L16.5359 8.26979C16.5478 8.2859 16.5589 8.30247 16.5694 8.31955L16.5836 8.34395C16.5909 8.35713 16.5978 8.37073 16.6043 8.38459L16.6193 8.4191C16.6245 8.43227 16.6293 8.44546 16.6337 8.45883C16.6369 8.46876 16.64 8.47901 16.6429 8.48933C16.6469 8.5037 16.6503 8.51816 16.6533 8.53279C16.6556 8.54441 16.6577 8.55602 16.6594 8.56768C16.6605 8.5745 16.6614 8.58169 16.6623 8.58891L16.6643 8.60917C16.6653 8.62115 16.666 8.63315 16.6664 8.64515L16.6667 8.66666V12H15.3334V9.33332H12.6667C12.3249 9.33332 12.0431 9.07596 12.0046 8.7444L12.0001 8.66666V5.99999H6.00008V18H12.0001V19.3333H6.00008C5.2637 19.3333 4.66675 18.7364 4.66675 18V5.99999C4.66675 5.26361 5.2637 4.66666 6.00008 4.66666H12.6667ZM16.5388 12.8667C17.716 12.8667 18.5581 13.6422 18.6667 14.6371H17.2451C17.1636 14.2924 16.9735 14.0261 16.5298 14.0261H15.579C15.0538 14.0261 14.7188 14.4099 14.7188 14.8721L14.7279 17.3278C14.7279 17.79 15.0629 18.1739 15.5881 18.1739H16.5388C16.9735 18.1739 17.1546 17.9232 17.2451 17.5864H18.6758C18.5671 18.5656 17.716 19.3333 16.5479 19.3333H15.579C14.3476 19.3333 13.3425 18.4638 13.3425 17.3983L13.3334 14.8095C13.3334 13.7362 14.3385 12.8667 15.57 12.8667H16.5388ZM13.3334 6.94332V7.99999H14.3901L13.3334 6.94332Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,11 @@
<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="#3370FF"/>
<g clip-path="url(#clip0_2905_27805)">
<path d="M15.3334 5.33334C15.7016 5.33334 16.0001 5.63182 16.0001 6.00001V6.66668H18.0001C18.7365 6.66668 19.3334 7.26363 19.3334 8.00001V17.3333C19.3334 18.0697 18.7365 18.6667 18.0001 18.6667H6.00008C5.2637 18.6667 4.66675 18.0697 4.66675 17.3333V8.00001C4.66675 7.26363 5.2637 6.66668 6.00008 6.66668H8.00008V6.00001C8.00008 5.63182 8.29856 5.33334 8.66675 5.33334C9.03494 5.33334 9.33341 5.63182 9.33341 6.00001V6.66668H14.6667V6.00001C14.6667 5.63182 14.9652 5.33334 15.3334 5.33334ZM8.00008 8.00001H6.00008V17.3333H18.0001V8.00001H16.0001V8.66668C16.0001 9.03487 15.7016 9.33334 15.3334 9.33334C14.9652 9.33334 14.6667 9.03487 14.6667 8.66668V8.00001H9.33341V8.66668C9.33341 9.03487 9.03494 9.33334 8.66675 9.33334C8.29856 9.33334 8.00008 9.03487 8.00008 8.66668V8.00001ZM15.3334 13.6667C15.7016 13.6667 16.0001 13.9652 16.0001 14.3333C16.0001 14.7015 15.7016 15 15.3334 15H8.66675C8.29856 15 8.00008 14.7015 8.00008 14.3333C8.00008 13.9652 8.29856 13.6667 8.66675 13.6667H15.3334ZM12.3334 11C12.7016 11 13.0001 11.2985 13.0001 11.6667C13.0001 12.0349 12.7016 12.3333 12.3334 12.3333H8.66675C8.29856 12.3333 8.00008 12.0349 8.00008 11.6667C8.00008 11.2985 8.29856 11 8.66675 11H12.3334Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2905_27805">
<rect width="16" height="16" fill="white" transform="translate(4 4)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -46,6 +46,7 @@
v-if="attrs.selectorType === 'checkbox'"
:value="getChecked(record)"
:indeterminate="getIndeterminate(record)"
:disabled="isDisabledChildren(record)"
@click.stop
@change="rowSelectChange(record)"
/>
@ -329,6 +330,10 @@
excludeKeys: Set<string>;
selectorStatus: SelectAllEnum;
actionConfig?: BatchActionConfig;
disabledConfig?: {
disabledChildren?: boolean;
parentKey?: string;
};
noDisable?: boolean;
showSetting?: boolean;
columns: MsTableColumn;
@ -737,6 +742,14 @@
return false;
}
function isDisabledChildren(record: TableData) {
if (!props.disabledConfig?.disabledChildren) {
return false;
}
//
return !!record[props.disabledConfig.parentKey || 'parent'];
}
onMounted(async () => {
await initColumn();
batchLeft.value = getBatchLeft();

View File

@ -1,5 +1,7 @@
import { cloneDeep } from 'lodash-es';
import type { PassRateCountDetail, planStatusType, TestPlanDetail } from '@/models/testPlan/testPlan';
import type { PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
import type { countDetail, PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
import { LastExecuteResults } from '@/enums/caseEnum';
// TODO: 对照后端字段
// 测试计划详情
@ -55,6 +57,14 @@ export const defaultExecuteForm = {
planCommentFileIds: [],
notifier: [] as string[],
};
export const defaultCount: countDetail = {
success: 0,
error: 0,
fakeError: 0,
block: 0,
pending: 0,
};
// 报告详情
export const defaultReportDetail: PlanReportDetail = {
id: '',
@ -68,20 +78,10 @@ export const defaultReportDetail: PlanReportDetail = {
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,
},
executeCount: cloneDeep(defaultCount),
functionalCount: cloneDeep(defaultCount),
apiCaseCount: cloneDeep(defaultCount),
apiScenarioCount: cloneDeep(defaultCount),
};
export const statusConfig: StatusListType[] = [
@ -102,15 +102,14 @@ export const statusConfig: StatusListType[] = [
key: 'SUCCESS',
},
// TODO 这个版本不展示误报
// {
// label: 'common.fakeError',
// value: 'fakeError',
// color: '#FFC14E',
// class: 'bg-[rgb(var(--warning-6))]',
// rateKey: 'requestFakeErrorRate',
// key: 'FAKE_ERROR',
// },
{
label: 'common.fakeError',
value: 'fakeError',
color: '#FFC14E',
class: 'bg-[rgb(var(--warning-6))]',
rateKey: 'requestFakeErrorRate',
key: 'FAKE_ERROR',
},
{
label: 'common.block',
value: 'block',

View File

@ -19,11 +19,12 @@ export interface PlanReportDetail {
caseTotal: number;
executeCount: countDetail;
functionalCount: countDetail;
// TOTO 这个版本不展示场景和接口
// apiCaseCount: countDetail; // 接口场景用例分析-用例数
// apiScenarioCount: countDetail; // 接口场景用例分析-用例数
apiCaseCount: countDetail; // 接口场景用例分析-用例数
apiScenarioCount: countDetail; // 接口场景用例分析-用例数
}
export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO';
export interface ReportMetricsItemModel {
unit: string;
value: number | string;

View File

@ -3,8 +3,8 @@
<div class="mb-4 flex items-center justify-between">
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="All">{{ t('report.all') }}</a-radio>
<a-radio value="INDEPENDENT">{{ t('report.independent') }}</a-radio>
<a-radio value="INTEGRATED">{{ t('report.collection') }}</a-radio>
<a-radio value="INDEPENDENT">{{ t('report.detail.testReport') }}</a-radio>
<a-radio value="INTEGRATED">{{ t('report.detail.testPlanGroupReport') }}</a-radio>
</a-radio-group>
<div class="items-right flex gap-[8px]">
<a-input-search

View File

@ -65,6 +65,18 @@
{{ statusExecuteRate.blockRateResult }}
</td>
</tr>
<tr v-if="props.status === 'fakeError'" class="popover-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.fail') }}</div>
</td>
<td class="popover-value-td-count">
{{ addCommasToNumber(countDetailData.fakeError) }}
</td>
<td class="popover-value-td-pass">
{{ statusExecuteRate.errorRateResult }}
</td>
</tr>
<tr v-if="props.status === 'error'" 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>
@ -88,16 +100,18 @@
import MsColorLine from '@/components/pure/ms-color-line/index.vue';
import { statusConfig } from '@/config/testPlan';
import { defaultCount, statusConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type { countDetail, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import type { AnalysisType, countDetail, PlanReportDetail } from '@/models/testPlan/testPlanReport';
const { t } = useI18n();
const props = defineProps<{
detail: PlanReportDetail;
status: keyof countDetail;
type: AnalysisType;
}>();
const defaultStatus = {
@ -113,8 +127,15 @@
return statusConfig.find((e) => e.value === props.status) || defaultStatus;
});
const analysisTypeMap: Record<AnalysisType, keyof PlanReportDetail> = {
FUNCTIONAL: 'functionalCount',
API: 'apiCaseCount',
SCENARIO: 'apiScenarioCount',
};
const countDetailData = computed(() => {
return props.detail.functionalCount;
const countKey = analysisTypeMap[props.type] as keyof PlanReportDetail;
return props.detail[countKey] ? (props.detail[countKey] as countDetail) : defaultCount;
});
const colorData = computed(() => {

View File

@ -1,16 +1,5 @@
<template>
<MsCard v-if="!props.isDrawer" class="mb-[16px]" hide-back hide-footer auto-height no-content-padding hide-divider>
<template #headerLeft>
<div class="flex items-center font-medium">
<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>
<PlanDetailHeaderRight :share-id="shareId" :detail="detail" />
</template>
</MsCard>
<ReportHeader v-if="!props.isDrawer" :detail="detail" :share-id="shareId" />
<div class="analysis-wrapper">
<div class="analysis min-w-[238px]">
<div class="block-title">{{ t('report.detail.api.reportAnalysis') }}</div>
@ -30,16 +19,16 @@
/>
</div>
</div>
<!-- TODO 接口用例&场景用例待联调 -->
<div class="analysis-wrapper">
<div class="analysis min-w-[330px]">
<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" />
<SingleStatusProgress :detail="detail" type="FUNCTIONAL" status="pending" />
<SingleStatusProgress :detail="detail" type="FUNCTIONAL" status="success" />
<SingleStatusProgress :detail="detail" type="FUNCTIONAL" status="block" />
<SingleStatusProgress :detail="detail" type="FUNCTIONAL" status="error" />
</div>
<div class="relative w-[30%] min-w-[150px]">
<div class="charts absolute w-full text-center">
@ -62,14 +51,15 @@
</div>
</div>
</div>
<!-- TODO 接口用例&场景用例待联调 -->
<div class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.apiUseCaseAnalysis') }}</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" />
<SingleStatusProgress type="API" :detail="detail" status="pending" />
<SingleStatusProgress type="API" :detail="detail" status="success" />
<SingleStatusProgress type="API" :detail="detail" status="fakeError" />
<SingleStatusProgress type="API" :detail="detail" status="error" />
</div>
<div class="relative w-[30%] min-w-[150px]">
<div class="charts absolute w-full text-center">
@ -96,10 +86,10 @@
<div class="block-title">{{ t('report.detail.scenarioUseCaseAnalysis') }}</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" />
<SingleStatusProgress type="SCENARIO" :detail="detail" status="pending" />
<SingleStatusProgress type="SCENARIO" :detail="detail" status="success" />
<SingleStatusProgress type="SCENARIO" :detail="detail" status="fakeError" />
<SingleStatusProgress type="SCENARIO" :detail="detail" status="error" />
</div>
<div class="relative w-[30%] min-w-[150px]">
<div class="charts absolute w-full text-center">
@ -123,35 +113,14 @@
</div>
</div>
</div>
<MsCard class="mb-[16px]" simple auto-height auto-width>
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
<div
:class="`${hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId ? '' : 'cursor-not-allowed'}`"
>
<MsRichText
v-model:raw="richText.summary"
v-model:filedIds="richText.richTextTmpFileIds"
:upload-image="handleUploadImage"
:preview-url="PreviewEditorImageUrl"
class="mt-[8px] w-full"
:editable="!!shareId"
/>
<MsFormItemSub
v-if="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && showButton"
:text="t('report.detail.oneClickSummary')"
:show-fill-icon="true"
@fill="handleSummary"
/>
</div>
<div
v-show="showButton && hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId"
class="mt-[16px] flex items-center gap-[12px]"
>
<a-button type="primary" @click="handleUpdateReportDetail">{{ t('common.save') }}</a-button>
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
</div>
</MsCard>
<Summary
v-model:richText="richText"
:share-id="shareId"
:show-button="showButton"
@update-summary="handleUpdateReportDetail"
@cancel="handleCancel"
@handle-summary="handleSummary"
/>
<MsCard simple auto-height auto-width>
<MsTab
v-model:active-key="activeTab"
@ -176,27 +145,29 @@
import MsChart from '@/components/pure/chart/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 MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import PlanDetailHeaderRight from './planDetailHeaderRight.vue';
import ReportMetricsItem from './ReportMetricsItem.vue';
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
import ApiCaseTable from '@/views/test-plan/report/detail/component/apiCaseTable.vue';
import BugTable from '@/views/test-plan/report/detail/component/bugTable.vue';
import FeatureCaseTable from '@/views/test-plan/report/detail/component/featureCaseTable.vue';
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
import ScenarioCaseTable from '@/views/test-plan/report/detail/component/scenarioCaseTable.vue';
import Summary from '@/views/test-plan/report/detail/component/summary.vue';
import { editorUploadFile, updateReportDetail } from '@/api/modules/test-plan/report';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { defaultReportDetail, statusConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import type { LegendData } from '@/models/apiTest/report';
import type { PlanReportDetail, ReportMetricsItemModel, StatusListType } from '@/models/testPlan/testPlanReport';
import type {
countDetail,
PlanReportDetail,
ReportMetricsItemModel,
StatusListType,
} from '@/models/testPlan/testPlanReport';
import { getIndicators } from '@/views/api-test/report/utils';
@ -213,7 +184,7 @@
}>();
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const showButton = ref(false);
const showButton = ref<boolean>(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
@ -373,13 +344,6 @@
});
}
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
async function handleUpdateReportDetail() {
try {
await updateReportDetail({
@ -395,10 +359,6 @@
console.log(error);
}
}
function handleCancel() {
richText.value = { summary: detail.value.summary };
showButton.value = false;
}
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{
@ -471,10 +431,68 @@
});
});
const summaryContent = ref<string>(`
<p style=""><span color="" fontsize="">本次完成 测试计划名称功能测试接口测试 300 用例已执行 285 未执行 15 执行率为 95%通过用例 270 通过率为 90%达到/未达到通过阈值通过阈值为85%xxx计划满足/不满足发布要求<br>1本次测试包含100条功能测试用例执行了95条未执行5条执行率为95%通过用例90条通过率为90%共发现缺陷0个<br>2本次测试包含100条接口测试用例执行了95条未执行5条执行率为95%通过用例90条通过率为90%共发现缺陷0个<br>3本次测试包含100条场景测试用例执行了95条未执行5条执行率为95%通过用例90条通过率为90%共发现缺陷0个</span></p>
`);
// TODO
function getSummaryDetail(detailCount: countDetail) {
if (detailCount) {
const { success, error, fakeError, pending, block } = detailCount;
//
const hasExecutedCase = success + error + fakeError + block;
//
const caseTotal = hasExecutedCase + pending;
//
const executedCount = (hasExecutedCase / caseTotal) * 100;
const apiExecutedRate = `${Number.isNaN(executedCount) ? 0 : executedCount.toFixed(2)}%`;
//
const successCount = (success / caseTotal) * 100;
const successRate = `${Number.isNaN(successCount) ? 0 : successCount.toFixed(2)}%`;
return {
hasExecutedCase,
caseTotal,
apiExecutedRate,
successRate,
pending,
success,
};
}
return {
hasExecutedCase: 0,
caseTotal: 0,
apiExecutedRate: 0,
successRate: 0,
pending: 0,
success: 0,
};
}
const summaryContent = computed(() => {
const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value;
const functionalCaseDetail = getSummaryDetail(functionalCount);
const apiCaseDetail = getSummaryDetail(apiCaseCount);
const apiScenarioDetail = getSummaryDetail(apiScenarioCount);
const allCaseTotal = functionalCaseDetail.caseTotal + apiCaseDetail.caseTotal + apiScenarioDetail.caseTotal;
const allHasExecutedCase =
functionalCaseDetail.hasExecutedCase + apiCaseDetail.hasExecutedCase + apiScenarioDetail.hasExecutedCase;
const allPendingCase = functionalCaseDetail.pending + apiCaseDetail.pending + apiScenarioDetail.pending;
const allSuccessCase = functionalCaseDetail.success + apiCaseDetail.success + apiScenarioDetail.success;
const allExecutedCount = (allHasExecutedCase / allCaseTotal) * 100;
const allExecutedRate = `${Number.isNaN(allExecutedCount) ? 0 : allExecutedCount.toFixed(2)}%`;
//
const allSuccessCount = (allSuccessCase / allCaseTotal) * 100;
const allSuccessRate = `${Number.isNaN(allSuccessCount) ? 0 : allSuccessCount.toFixed(2)}%`;
//
return `<p style=""><span color="" fontsize="">本次完成 ${detail.value.name}的功能测试,接口测试;共 ${allCaseTotal}条 用例,已执行 ${allHasExecutedCase} 条,未执行 ${allPendingCase} 条,执行率为 ${allExecutedRate}%,通过用例 ${allSuccessCase} 条,通过率为 ${allSuccessRate},达到/未达到通过阈值(通过阈值为${detail.value.passThreshold}%xxx计划满足/不满足发布要求。<br>
1本次测试包含${functionalCaseDetail.caseTotal}条功能测试用例执行了${functionalCaseDetail.hasExecutedCase}未执行${functionalCaseDetail.pending}执行率为${functionalCaseDetail.apiExecutedRate}通过用例${functionalCaseDetail.success}通过率为${functionalCaseDetail.successRate}共发现缺陷0个<br>
2本次测试包含${apiCaseDetail.caseTotal}条接口测试用例执行了${apiCaseDetail.hasExecutedCase}未执行${apiCaseDetail.pending}执行率为${apiCaseDetail.apiExecutedRate}通过用例${apiCaseDetail.success}通过率为${apiCaseDetail.successRate}共发现缺陷0个<br>
3本次测试包含${apiScenarioDetail.caseTotal}条场景测试用例执行了${apiScenarioDetail.hasExecutedCase}未执行${apiScenarioDetail.pending}执行率为${apiScenarioDetail.apiExecutedRate}%通过用例${apiScenarioDetail.success}通过率为${apiScenarioDetail.successRate}共发现缺陷0个</span></p>
`;
});
function handleCancel() {
richText.value = { summary: detail.value.summary };
showButton.value = false;
}
function handleSummary() {
richText.value.summary = summaryContent.value;
}

View File

@ -0,0 +1,246 @@
<template>
<ReportHeader v-if="!props.isDrawer" :detail="detail" :share-id="shareId" />
<div class="analysis-wrapper">
<div class="analysis min-w-[238px]">
<div class="block-title">{{ t('report.detail.api.reportAnalysis') }}</div>
<ReportMetricsItem
v-for="analysisItem in reportAnalysisList"
:key="analysisItem.name"
:item-info="analysisItem"
/>
</div>
<div class="analysis min-w-[410px]">
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
<SetReportChart
size="160px"
:legend-data="legendData"
:options="charOptions"
:request-total="getIndicators(detail.caseTotal) || 0"
/>
</div>
</div>
<Summary
v-model:richText="richText"
:share-id="shareId"
:show-button="showButton"
@update-summary="handleUpdateReportDetail"
@cancel="handleCancel"
@handle-summary="handleSummary"
/>
<MsCard>
<div class="flex items-center justify-between">
<div class="block-title">{{ t('report.detail.api.reportDetail') }}</div>
<a-radio-group class="mb-2" :model-value="currentMode" type="button" @change="handleModeChange">
<a-radio value="drawer">
<div class="mode-button">
<MsIcon :class="{ 'active-color': currentMode === 'drawer' }" type="icon-icon_drawer" />
<span class="mode-button-title">{{ t('msTable.columnSetting.drawer') }}</span>
</div>
</a-radio>
<a-radio value="new_window">
<div class="mode-button">
<MsIcon :class="{ 'active-color': currentMode === 'new_window' }" type="icon-icon_into-item_outlined" />
<span class="mode-button-title">{{ t('msTable.columnSetting.newWindow') }}</span>
</div>
</a-radio>
</a-radio-group>
</div>
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue';
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
import ReportMetricsItem from '@/views/test-plan/report/detail/component/ReportMetricsItem.vue';
import Summary from '@/views/test-plan/report/detail/component/summary.vue';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { defaultReportDetail, statusConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type { LegendData } from '@/models/apiTest/report';
import type {
countDetail,
PlanReportDetail,
ReportMetricsItemModel,
StatusListType,
} from '@/models/testPlan/testPlanReport';
import { getIndicators } from '@/views/api-test/report/utils';
const { t } = useI18n();
const route = useRoute();
const props = defineProps<{
detailInfo: PlanReportDetail;
isDrawer?: boolean;
}>();
const emit = defineEmits<{
(e: 'updateSuccess'): void;
}>();
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const shareId = ref<string>(route.query.shareId as string);
const charOptions = ref({
tooltip: {
show: false,
trigger: 'item',
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['62%', '80%'],
center: ['50%', '50%'],
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 legendData = ref<LegendData[]>([]);
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{
name: t('report.detail.testPlanTotal'),
value: detail.value.passThreshold,
unit: t('report.detail.number'),
icon: 'plan_total',
},
{
name: t('report.detail.testPlanCaseTotal'),
value: detail.value.passRate,
unit: t('report.detail.number'),
icon: 'case_total',
},
{
name: t('report.passRate'),
value: detail.value.executeRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.totalDefects'),
value: addCommasToNumber(detail.value.bugCount),
unit: t('report.detail.number'),
icon: 'bugTotal',
},
]);
const summaryContent = ref<string>('');
const showButton = ref(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
async function handleUpdateReportDetail() {
try {
await updateReportDetail({
id: detail.value.id,
summary: richText.value.summary,
richTextTmpFileIds: richText.value.richTextTmpFileIds ?? [],
});
Message.success(t('common.updateSuccess'));
showButton.value = false;
emit('updateSuccess');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
function handleCancel() {
richText.value = { summary: detail.value.summary };
showButton.value = false;
}
function handleSummary() {
richText.value.summary = summaryContent.value;
}
const currentMode = ref('');
const handleModeChange = (value: string | number | boolean) => {
currentMode.value = value as string;
};
</script>
<style scoped lang="less">
.block-title {
@apply mb-4 font-medium;
}
.analysis-wrapper {
@apply mb-4 flex flex-wrap items-center gap-4;
.analysis {
padding: 24px;
height: 250px;
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply flex-1 rounded-xl bg-white;
.charts {
top: 36%;
right: 0;
bottom: 0;
left: 0;
z-index: 99;
margin: auto;
}
}
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<MsCard v-if="!props.isDrawer" class="mb-[16px]" hide-back hide-footer auto-height no-content-padding hide-divider>
<template #headerLeft>
<div class="flex items-center font-medium">
<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>
<PlanDetailHeaderRight :share-id="shareId" :detail="detail" />
</template>
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import PlanDetailHeaderRight from '@/views/test-plan/report/detail/component/planDetailHeaderRight.vue';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
const props = defineProps<{
detail: PlanReportDetail;
shareId?: string;
isDrawer?: boolean;
}>();
</script>
<style scoped></style>

View File

@ -0,0 +1,81 @@
<template>
<MsCard class="mb-[16px]" simple auto-height auto-width>
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
<div
:class="`${hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId ? '' : 'cursor-not-allowed'}`"
>
<MsRichText
v-model:raw="innerSummary.summary"
v-model:filedIds="innerSummary.richTextTmpFileIds"
:upload-image="handleUploadImage"
:preview-url="PreviewEditorImageUrl"
class="mt-[8px] w-full"
:editable="!!shareId"
/>
<MsFormItemSub
v-if="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && props.showButton"
:text="t('report.detail.oneClickSummary')"
:show-fill-icon="true"
@fill="handleSummary"
/>
</div>
<div
v-show="props.showButton && hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId"
class="mt-[16px] flex items-center gap-[12px]"
>
<a-button type="primary" @click="handleUpdateReportDetail">{{ t('common.save') }}</a-button>
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
</div>
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import { editorUploadFile } from '@/api/modules/test-plan/report';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
const { t } = useI18n();
const props = defineProps<{
richText: { summary: string; richTextTmpFileIds?: string[] };
shareId?: string;
showButton: boolean;
}>();
const emit = defineEmits<{
(e: 'updateSummary'): void;
(e: 'cancel'): void;
(e: 'handleSummary'): void;
}>();
const innerSummary = useVModel(props, 'richText', emit);
function handleCancel() {
emit('cancel');
}
function handleUpdateReportDetail() {
emit('updateSummary');
}
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
function handleSummary() {
emit('handleSummary');
}
</script>
<style scoped></style>

View File

@ -0,0 +1,36 @@
<template>
<PlanGroupDetail :detail-info="detail" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
import { getReportDetail } from '@/api/modules/test-plan/report';
import { defaultReportDetail } from '@/config/testPlan';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
const route = useRoute();
const reportId = ref<string>(route.query.id as string);
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
async function getDetail() {
try {
detail.value = await getReportDetail(reportId.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
getDetail();
});
</script>
<style scoped></style>

View File

@ -40,4 +40,6 @@ export default {
'report.detail.apiCaseDetails': 'Api use case details',
'report.detail.scenarioCaseDetails': 'Scenario use case details',
'report.detail.oneClickSummary': 'One click report summary',
'report.detail.testPlanTotal': 'Total plan',
'report.detail.testPlanCaseTotal': 'Total use cases',
};

View File

@ -40,4 +40,8 @@ export default {
'report.detail.apiCaseDetails': '接口用例明细',
'report.detail.scenarioCaseDetails': '场景用例明细',
'report.detail.oneClickSummary': '一键填写报告总结',
'report.detail.testReport': '测试报告',
'report.detail.testPlanGroupReport': '测试组报告',
'report.detail.testPlanTotal': '计划总数',
'report.detail.testPlanCaseTotal': '用例总数',
};

View File

@ -39,6 +39,10 @@
:selectable="hasOperationPermission && showType !== testPlanTypeEnum.ALL"
filter-icon-align-left
:expanded-keys="expandedKeys"
:disabled-config="{
disabledChildren: true,
parentKey: 'parent',
}"
v-on="propsEvent"
@batch-action="handleTableBatch"
@filter-change="filterChange"
@ -365,6 +369,7 @@
batchArchivedPlan,
batchCopyPlan,
batchDeletePlan,
batchEditTestPlan,
batchMovePlan,
deletePlan,
deleteScheduleTask,
@ -383,7 +388,7 @@
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { DragSortParams, ModuleTreeNode } from '@/models/common';
import { DragSortParams, ModuleTreeNode, TableQueryParams } from '@/models/common';
import type {
AddTestPlanParams,
BatchMoveParams,
@ -993,9 +998,41 @@
}
/**
* 打开关闭定时任务 TODO 待联调
* 打开关闭定时任务
*/
function handleStatusTimingTask(enable: boolean) {}
async function handleStatusTimingTask(enable: boolean) {
const filterParams = {
...propsRes.value.filter,
};
if (isArchived.value) {
filterParams.status = ['ARCHIVED'];
}
try {
const { selectedIds, selectAll, excludeIds } = batchParams.value;
const params: TableQueryParams = {
selectIds: selectedIds || [],
selectAll: !!selectAll,
excludeIds: excludeIds || [],
projectId: appStore.currentProjectId,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
condition: {
filter: filterParams,
keyword: keyword.value,
},
type: showType.value,
scheduleOpen: enable,
};
await batchEditTestPlan(params);
Message.success(
enable
? t('testPlan.testPlanGroup.enableScheduleTaskSuccess')
: t('testPlan.testPlanGroup.closeScheduleTaskSuccess')
);
fetchData();
} catch (error) {
console.log(error);
}
}
/**
* 归档测试计划以及计划组

View File

@ -161,7 +161,7 @@
await configSchedule(params);
handleCancel();
emit('handleSuccess');
Message.success(t('common.createSuccess'));
Message.success(props.taskConfig ? t('common.updateSuccess') : t('common.createSuccess'));
}
} catch (error) {
// eslint-disable-next-line no-console

View File

@ -134,4 +134,6 @@ export default {
'testPlan.testPlanGroup.batchArchivedGroup': 'Confirm archive: {count} test plan groups',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': 'Are you sure to delete {count} test plan groups?',
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': 'Delete the scheduled task successfully',
'testPlan.testPlanGroup.enableScheduleTaskSuccess': 'Start the scheduled task successfully',
'testPlan.testPlanGroup.closeScheduleTaskSuccess': 'Scheduled mission closed successfully',
};

View File

@ -123,4 +123,6 @@ export default {
'testPlan.testPlanGroup.batchArchivedGroup': '确认归档:{count} 个测试计划组吗',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': '确认删除 {count} 个测试计划组吗?',
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': '删除定时任务成功',
'testPlan.testPlanGroup.enableScheduleTaskSuccess': '开启定时任务成功',
'testPlan.testPlanGroup.closeScheduleTaskSuccess': '关闭定时任务成功',
};