feat(测试计划): 测试计划报告自定义配置以报告及预览页面初稿待联调&计划组列表调整

This commit is contained in:
xinxin.wu 2024-07-03 19:16:37 +08:00 committed by 刘瑞斌
parent 3e49eab864
commit e8ffc7b3b3
41 changed files with 1979 additions and 851 deletions

View File

@ -100,6 +100,7 @@
placeholder: 'editor.placeholder',
draggable: false,
autoHeight: true,
editable: true,
}
);
@ -137,6 +138,14 @@
if (props.raw !== editor.value?.getHTML()) {
editor.value?.commands.setContent(props.raw);
}
}
);
watch(
() => props.editable,
(val) => {
//
editor.value?.setOptions({ editable: val });
},
{
immediate: true,
@ -347,7 +356,7 @@
}),
],
autofocus: false,
editable: !props.editable,
editable: props.editable,
onUpdate: () => {
debounceOnUpdate();
},

View File

@ -62,6 +62,7 @@ export enum TestPlanRouteEnum {
TEST_PLAN = 'testPlan',
TEST_PLAN_INDEX = 'testPlanIndex',
TEST_PLAN_INDEX_DETAIL = 'testPlanIndexDetail',
TEST_PLAN_INDEX_CONFIG = 'testPlanIndexConfig',
TEST_PLAN_INDEX_DETAIL_FEATURE_CASE_DETAIL = 'testPlanIndexDetailFeatureCaseDetail',
TEST_PLAN_REPORT = 'testPlanReport',
TEST_PLAN_REPORT_DETAIL = 'testPlanReportDetail',

View File

@ -0,0 +1,11 @@
export enum ReportCardTypeEnum {
SUMMARY = 'SUMMARY', // 报告总结
BUG_DETAIL = 'BUG_DETAIL', // 缺陷明细
FUNCTIONAL_DETAIL = 'FUNCTIONAL_DETAIL', // 功能用例明细
API_CASE_DETAIL = 'API_CASE_DETAIL', // 接口用例明细
SCENARIO_CASE_DETAIL = 'SCENARIO_CASE_DETAIL', // 场景用例明细
SUB_PLAN_DETAIL = 'SUB_PLAN_DETAIL', // 计划组子计划详情
CUSTOM_CARD = 'CUSTOM_CARD', // 自定义卡片
}
export default {};

View File

@ -0,0 +1,31 @@
import { ref } from 'vue';
/**
* @description
*/
export default function useDoubleClick(callback: () => void) {
const count = ref(0);
const lastClickTime = ref(0);
const DOUBLE_CLICK_THRESHOLD = 300; // 300毫秒
function handleClick() {
const currentTime = new Date().getTime();
const timeDiff = currentTime - lastClickTime.value;
if (timeDiff < DOUBLE_CLICK_THRESHOLD) {
count.value++;
} else {
count.value = 1;
}
lastClickTime.value = currentTime;
if (count.value >= 2) {
callback();
count.value = 0;
}
}
return {
handleClick,
};
}

View File

@ -1,4 +1,7 @@
// 实时
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
export interface RealTaskCenterApiCaseItem {
organizationName: string; // 所属组织
projectName: string;
@ -14,6 +17,13 @@ export interface RealTaskCenterApiCaseItem {
operationTime: string;
integrated: boolean; // 是否为集合报告
}
export interface TestPlanTaskCenterItem extends RealTaskCenterApiCaseItem {
children: TestPlanTaskCenterItem[];
childrenCount: number;
groupId: string;
type: keyof typeof testPlanTypeEnum;
}
// 定时任务
export interface TimingTaskCenterApiCaseItem {
organizationName: string;

View File

@ -34,4 +34,5 @@ export interface ApiOrScenarioCaseItem {
executeUser: string;
bugCount: number;
reportId: string;
projectId: string;
}

View File

@ -1,3 +1,5 @@
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
export interface countDetail {
success: number;
error: number;
@ -28,8 +30,11 @@ export interface PlanReportDetail {
apiBugCount: number; // 接口用例明细bug总数
scenarioBugCount: number; // 场景用例明细bug总数
testPlanName: string;
resultStatus?: string; // 报告结果
}
export type detailCountKey = 'functionalCount' | 'apiCaseCount' | 'apiScenarioCount';
export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO';
export interface ReportMetricsItemModel {
@ -49,3 +54,18 @@ export interface StatusListType {
rateKey: string;
key: string;
}
export interface configItem {
id: string;
value: ReportCardTypeEnum;
label: string;
content?: string;
system: boolean;
enableEdit: boolean;
}
export interface customValueForm {
content?: string;
label: string;
richTextTmpFileIds?: string[];
}

View File

@ -41,7 +41,7 @@ const TestPlan: AppRouteRecordRaw = {
{
path: 'testPlanReportDetail',
name: TestPlanRouteEnum.TEST_PLAN_REPORT_DETAIL,
component: () => import('@/views/test-plan/report/detail/index.vue'),
component: () => import('@/views/test-plan/report/detail/detail.vue'),
meta: {
locale: 'menu.apiTest.reportDetail',
roles: ['PROJECT_TEST_PLAN_REPORT:READ'],
@ -77,6 +77,17 @@ const TestPlan: AppRouteRecordRaw = {
],
},
},
// 自定义配置报告
{
path: 'testPlanIndexConfig',
name: TestPlanRouteEnum.TEST_PLAN_INDEX_CONFIG,
component: () => import('@/views/test-plan/report/detail/configReport.vue'),
meta: {
locale: 'testPlan.planConfigReport',
roles: ['PROJECT_TEST_PLAN_REPORT:READ'],
isTopMenu: false,
},
},
// 测试计划-测试计划详情-功能用例详情
{
path: 'testPlanIndexDetailFeatureCaseDetail',

View File

@ -56,7 +56,7 @@
import ReportDetailHeader from './reportDetailHeader.vue';
import reportInfoHeader from './step/reportInfoHeaders.vue';
import TiledList from './tiledList.vue';
import ReportMetricsItem from '@/views/test-plan/report/detail/component/ReportMetricsItem.vue';
import ReportMetricsItem from '@/views/test-plan/report/detail/component/system-card/ReportMetricsItem.vue';
import { toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';

View File

@ -33,7 +33,9 @@
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && !props.isDisabledTestPlan"
#actualResult="{ record }"
>
<div v-if="props.isPreview">{{ record.actualResult }}</div>
<a-textarea
v-else
v-model="record.actualResult"
:max-length="1000"
size="mini"
@ -44,7 +46,7 @@
</template>
<template #lastExecResult="{ record }">
<a-select
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && !props.isDisabledTestPlan"
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && !props.isDisabledTestPlan && !props.isPreview"
v-model:model-value="record.executeResult"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full"
@ -105,6 +107,7 @@
isScrollY?: boolean;
isTestPlan?: boolean;
isDisabledTestPlan?: boolean;
isPreview?: boolean; //
}>(),
{
isDisabled: false,

View File

@ -19,17 +19,21 @@
ref="tableRef"
:action-config="tableBatchActions"
:selectable="hasOperationPermission"
:expanded-keys="expandedKeys"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<!-- TOTO 等待联调 后台接口需要调整 -->
<template #resourceNum="{ record }">
<div
v-if="!record.integrated"
type="text"
class="one-line-text w-full"
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
@click="showDetail(record)"
>{{ record.resourceNum }}
<div class="flex items-center">
<PlanExpandRow
v-model:expanded-keys="expandedKeys"
num-key="resourceNum"
:record="record"
:permission="permissionsMap[props.group].jump"
@action="showDetail(record)"
@expand="expandHandler(record)"
/>
</div>
</template>
<template #resourceName="{ record }">
@ -114,6 +118,7 @@
import useTable from '@/components/pure/ms-table/useTable';
import ExecStatus from '@/views/test-plan/report/component/execStatus.vue';
import ExecutionStatus from '@/views/test-plan/report/component/reportStatus.vue';
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
import {
batchStopRealOrgPlan,
@ -134,6 +139,7 @@
import { hasAnyPermission } from '@/utils/permission';
import { BatchApiParams } from '@/models/common';
import type { TestPlanTaskCenterItem } from '@/models/projectManagement/taskCenter';
import { ReportExecStatus } from '@/enums/apiEnum';
import { PlanReportStatus } from '@/enums/reportEnum';
import { RouteEnum } from '@/enums/routeEnum';
@ -464,12 +470,22 @@
});
}
const expandedKeys = ref<string[]>([]);
function expandHandler(record: TestPlanTaskCenterItem) {
if (expandedKeys.value.includes(record.id)) {
expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
} else {
expandedKeys.value = [...expandedKeys.value, record.id];
}
}
function searchList() {
resetSelector();
initData();
}
onBeforeMount(async () => {
onBeforeMount(() => {
initData();
});
@ -487,4 +503,11 @@
await tableStore.initColumn(tableKeysMap[props.group], groupColumnsMap[props.group], 'drawer', true);
</script>
<style scoped></style>
<style scoped lang="less">
:deep(.arco-table-cell-expand-icon .arco-table-cell-inline-icon) {
display: none;
}
:deep(.arco-table-cell-align-left) > span:first-child {
padding-left: 0 !important;
}
</style>

View File

@ -0,0 +1,375 @@
<template>
<!-- 配置头开始 -->
<div class="report-name">
<div class="font-medium">
{{ t('testPlan.testPlanDetail.generateReport') }}
</div>
<div class="flex items-center">
<a-form ref="formRef" class="mt-1 max-w-[710px]" :model="reportForm">
<a-form-item
field="reportName"
asterisk-position="end"
:hide-label="true"
hide-asterisk
content-class="contentClass"
class="mb-0 max-w-[732px]"
>
<a-input
v-model:model-value="reportForm.reportName"
:placeholder="t('report.detail.enterReportNamePlaceHolder')"
:max-length="255"
class="w-[732px]"
:error="isError"
@input="inputHandler"
></a-input>
</a-form-item>
</a-form>
<div class="ml-[12px]">
<a-button type="secondary" @click="cancelHandler">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" @click="handleSave">{{ t('common.save') }}</a-button>
</div>
</div>
</div>
<!-- 配置头结束 -->
<!-- 报告头 -->
<div class="config-container">
<div class="config-left-container">
<div class="sticky top-[16px]">
<div class="mb-[16px] flex items-center justify-between">
<div class="flex items-center">
<div class="text-[16px] font-medium">{{ t('report.detail.baseField') }}</div>
<a-tooltip :content="t('report.detail.customFieldTooltip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<MsButton :disabled="!hasChange" class="cursor-pointer text-[rgb(var(--primary-5))]" @click="handleReset"
>{{ t('common.resetDefault') }}
</MsButton>
</div>
<!-- 自定义字段列表 -->
<VueDraggable
v-model="configList"
:sort="false"
class="custom-card-list w-full"
:group="{ name: 'report', pull: 'clone', put: false }"
:clone="onClone"
>
<div
v-for="item of configList"
v-show="showItem(item)"
:key="item.value"
:class="`${getHoverClass(item)} custom-card-item`"
@click.stop="addField(item)"
>
<a-tooltip :mouse-enter-delay="300" :content="t(item.label)" position="top">
<div class="flex items-center justify-between">
<div :class="`${getLabelClass(item)} custom-card-item-label one-line-text max-w-[calc(100%-14px)]`">{{
t(item.label)
}}</div>
<icon-close
v-if="!item.system"
:style="{ 'font-size': '14px' }"
class="cursor-pointer text-[var(--color-text-3)]"
@click.stop="removeField(item)"
/>
</div>
</a-tooltip>
</div>
<a-tooltip class="ms-tooltip-white" :disabled="!limitCustomLength">
<a-button type="outline" class="!h-[30px]" :disabled="limitCustomLength" @click.stop="addCustomField">
<div class="flex flex-row items-center gap-[8px]">
<icon-plus />
<span>{{ t('report.detail.customButton') }}</span>
</div>
</a-button>
<template #content>
<div class="text-[var(--color-text-1)]">{{ t('report.detail.customMaxNumber') }}</div>
</template>
</a-tooltip>
</VueDraggable>
</div>
</div>
<div class="config-right-container">
<ViewReport
v-model:card-list="cardItemList"
:detail-info="props.detailInfo"
:is-drawer="props.isDrawer"
:is-group="props.isGroup"
:is-preview="false"
@update-custom="updateCustom"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { cloneDeep, isEqual } from 'lodash-es';
import { VueDraggable } from 'vue-draggable-plus';
import MsButton from '@/components/pure/ms-button/index.vue';
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { defaultReportDetail } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId } from '@/utils';
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { defaultCustomConfig, defaultGroupConfig, defaultSingleConfig } from './reportConfig';
import { getSummaryDetail } from '@/views/test-plan/report/utils';
const { t } = useI18n();
const router = useRouter();
const props = defineProps<{
detailInfo: PlanReportDetail;
isDrawer?: boolean;
isGroup?: boolean;
}>();
const emit = defineEmits<{
(e: 'updateSuccess'): void;
}>();
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const showButton = ref<boolean>(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
const isError = ref(false);
const hasChange = ref(false);
const reportForm = ref({
reportName: '',
});
function inputHandler(value: string) {
if (value.trim().length === 0) {
isError.value = true;
}
isError.value = false;
}
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);
}
}
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
reportForm.value.reportName = detail.value.name;
}
});
const configList = ref<configItem[]>([]);
const cardItemList = ref<configItem[]>([]);
watch(
() => props.isGroup,
() => {
configList.value = props.isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
cardItemList.value = cloneDeep(configList.value);
},
{
immediate: true,
}
);
function getExist(cardItem: configItem) {
return cardItemList.value.find((item) => item.id === cardItem.id);
}
function getHoverClass(cardItem: configItem) {
if (getExist(cardItem) && !cardItem.system) {
return 'hover-selected-item-class';
}
if (!getExist(cardItem) && cardItem.system) {
return 'hover-item-class';
}
return '';
}
function getLabelClass(cardItem: configItem) {
const isSystemColor = cardItem.system ? 'cursor-not-allowed' : '';
return getExist(cardItem)
? `text-[var(--color-text-4)] ${isSystemColor}`
: `text-[var(--color-text-1)] cursor-pointer hover:text-[rgb(var(--primary-4))]`;
}
const limitCustomLength = computed(
() => configList.value.filter((item) => item.value === ReportCardTypeEnum.CUSTOM_CARD).length >= 10
);
const deleteCard = (cardItem: configItem) => {
cardItemList.value = cardItemList.value.filter((item) => item.id !== cardItem.id);
};
function addCustomField() {
if (limitCustomLength.value) {
return;
}
const id = getGenerateId();
configList.value.push({
...defaultCustomConfig,
id,
});
}
//
function handleReset() {
configList.value = props.isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
cardItemList.value = cloneDeep(configList.value);
nextTick(() => {
hasChange.value = false;
});
}
function resetConfigEditList(list: configItem[]) {
return list.map((item: configItem) => {
return {
...item,
enableEdit: false,
};
});
}
watch(
[() => configList.value, () => cardItemList.value],
() => {
const configValue = resetConfigEditList(configList.value);
const cardItemValue = resetConfigEditList(cardItemList.value);
const isisEqualList = props.isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
if (!isEqual(configValue, isisEqualList) || !isEqual(cardItemValue, isisEqualList)) {
nextTick(() => {
hasChange.value = true;
});
}
},
{ deep: true }
);
//
function removeField(currentItem: configItem) {
configList.value = configList.value.filter((item) => item.id !== currentItem.id);
deleteCard(currentItem);
}
//
function addField(cardItem: configItem) {
const isHasCard = cardItemList.value.find((item) => item.id === cardItem.id);
if (!isHasCard) {
cardItemList.value.push(cardItem);
}
}
//
function onClone(element: Record<'name' | 'id', string>) {
const isHasCard = cardItemList.value.find((item) => item.id === element.id);
if (!isHasCard) {
return element;
}
}
function cancelHandler() {
router.back();
}
//
function updateCustom(currentItem: configItem) {
const currentIndex = configList.value.findIndex((item) => item.id === currentItem.id);
configList.value.splice(currentIndex, 1, currentItem);
}
const functionalCaseTotal = computed(() => getSummaryDetail(detail.value.functionalCount).caseTotal);
const apiCaseTotal = computed(() => getSummaryDetail(detail.value.apiCaseCount).caseTotal);
const scenarioCaseTotal = computed(() => getSummaryDetail(detail.value.apiScenarioCount).caseTotal);
function showItem(item: configItem) {
switch (item.value) {
case ReportCardTypeEnum.FUNCTIONAL_DETAIL:
return functionalCaseTotal.value > 0;
case ReportCardTypeEnum.API_CASE_DETAIL:
return apiCaseTotal.value > 0;
case ReportCardTypeEnum.SCENARIO_CASE_DETAIL:
return scenarioCaseTotal.value > 0;
default:
return true;
}
}
//
function handleSave() {}
</script>
<style scoped lang="less">
.report-name {
padding: 0 16px;
height: 56px;
@apply flex items-center justify-between border-b bg-white;
}
.block-title {
@apply mb-4 font-medium;
}
.config-container {
@apply flex w-full;
.config-left-container {
margin-bottom: 16px;
padding: 16px;
width: 300px;
border-radius: 0 0 10px 10px;
box-sizing: border-box;
@apply bg-white;
.custom-card-list {
@apply grid grid-cols-2 gap-2;
.custom-card-item {
padding: 4px 8px;
border-radius: 4px;
background: var(--color-bg-3);
&.custom-button {
border: 1px solid rgb(var(--primary-5));
color: rgb(var(--primary-5));
@apply cursor-pointer;
}
}
}
}
.config-right-container {
padding: 16px;
width: calc(100% - 300px);
background: var(--color-bg-3);
}
}
.hover-item-class {
&:hover {
.custom-card-item-label {
color: rgb(var(--primary-4));
}
}
}
.hover-selected-item-class {
&:hover {
.custom-card-item-label {
color: rgb(var(--primary-3));
}
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<a-tooltip
v-if="!props.canEdit"
:mouse-enter-delay="300"
:disabled="!props.customForm.label"
:content="props.customForm.label"
position="tl"
>
<div class="one-line-text mb-[8px] font-medium">{{ innerTextForm.label }}</div>
</a-tooltip>
<a-input
v-else
v-model:model-value="innerTextForm.label"
:placeholder="t('report.detail.customTitlePlaceHolder')"
:max-length="255"
allow-clear
@blur="blurHandler"
/>
<div :class="`${hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId ? '' : 'cursor-not-allowed'}`">
<MsRichText
v-model:raw="innerTextForm.content"
v-model:filedIds="innerTextForm.richTextTmpFileIds"
:upload-image="handleUploadImage"
class="mt-[8px] w-full"
:editable="props.canEdit"
@click="handleClick"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import { editorUploadFile } from '@/api/modules/test-plan/report';
import useDoubleClick from '@/hooks/useDoubleClick';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
import { customValueForm } from '@/models/testPlan/testPlanReport';
const { t } = useI18n();
const props = defineProps<{
customForm: customValueForm;
canEdit: boolean;
shareId?: string;
currentId: string;
}>();
const emit = defineEmits<{
(e: 'updateCustom', formValue: customValueForm): void;
(e: 'dblclick'): void;
}>();
const innerTextForm = ref<customValueForm>({
content: '',
label: '',
richTextTmpFileIds: [],
});
watchEffect(() => {
innerTextForm.value = { ...props.customForm };
});
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
function blurHandler() {
emit('updateCustom', {
...innerTextForm.value,
label: innerTextForm.value.label || t('report.detail.customDefaultCardName'),
});
}
function emitDoubleClick() {
emit('dblclick');
}
const { handleClick } = useDoubleClick(emitDoubleClick);
</script>
<style scoped></style>

View File

@ -1,448 +0,0 @@
<template>
<ReportHeader v-if="!props.isDrawer" :detail="detail" :share-id="shareId" :is-group="false" />
<div class="analysis-wrapper" :data-cards="cardCount">
<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]">
<ExecuteAnalysis :detail="detail" />
</div>
<div v-if="functionalCaseTotal" class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.useCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<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">
<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-[80px] text-[var(--color-text-1)]">{{ functionCasePassRate }} </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)]">{{ functionCasePassRate }}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="functionCaseOptions"
/></div>
</div>
</div>
</div>
<div v-if="apiCaseTotal" class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.apiUseCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<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">
<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-[80px] text-[var(--color-text-1)]">{{ apiCasePassRate }} </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)]">{{ apiCasePassRate }}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="apiCaseOptions"
/></div>
</div>
</div>
</div>
<div v-if="scenarioCaseTotal" class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.scenarioUseCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<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">
<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-[80px] text-[var(--color-text-1)]">{{ scenarioCasePassRate }} </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)]">{{ scenarioCasePassRate }}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="scenarioCaseOptions"
/></div>
</div>
</div>
</div>
</div>
<Summary
v-model:richText="richText"
:share-id="shareId"
:show-button="showButton"
:is-plan-group="false"
:detail="detail"
@update-summary="handleUpdateReportDetail"
@cancel="handleCancel"
@handle-summary="handleSummary"
/>
<MsCard simple auto-height auto-width>
<MsTab
v-model:active-key="activeTab"
:show-badge="false"
:content-tab-list="contentTabList"
no-content
class="relative mb-[16px] border-b"
/>
<BugTable v-if="activeTab === 'bug'" :report-id="detail.id" :share-id="shareId" />
<FeatureCaseTable
v-if="activeTab === 'featureCase'"
:active-tab="activeTab"
:report-id="detail.id"
:share-id="shareId"
/>
<ApiAndScenarioTable
v-if="['apiCase', 'scenarioCase'].includes(activeTab)"
:report-id="detail.id"
:share-id="shareId"
:active-tab="activeTab"
/>
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useEventListener } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsChart from '@/components/pure/chart/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import ReportMetricsItem from './ReportMetricsItem.vue';
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
import ApiAndScenarioTable from '@/views/test-plan/report/detail/component/apiAndScenarioTable.vue';
import BugTable from '@/views/test-plan/report/detail/component/bugTable.vue';
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/executeAnalysis.vue';
import FeatureCaseTable from '@/views/test-plan/report/detail/component/featureCaseTable.vue';
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
import Summary from '@/views/test-plan/report/detail/component/summary.vue';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { commonConfig, defaultCount, defaultReportDetail, seriesConfig, statusConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type {
countDetail,
PlanReportDetail,
ReportMetricsItemModel,
StatusListType,
} from '@/models/testPlan/testPlanReport';
import { getSummaryDetail } from '@/views/test-plan/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 showButton = ref<boolean>(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
/**
* 分享share
*/
const shareId = ref<string>(route.query.shareId as string);
//
const functionCaseOptions = ref({
...commonConfig,
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
],
},
});
//
const apiCaseOptions = ref({
...commonConfig,
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
],
},
});
//
const scenarioCaseOptions = ref({
...commonConfig,
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
],
},
});
//
function getPassRateData(caseDetailCount: countDetail) {
const caseCountDetail = caseDetailCount || defaultCount;
const passRateData = statusConfig.filter((item) => ['success'].includes(item.value));
const { success } = caseCountDetail;
const valueList = success ? statusConfig : passRateData;
return valueList.map((item: StatusListType) => {
return {
value: caseCountDetail[item.value] || 0,
name: t(item.label),
itemStyle: {
color: success ? item.color : '#D4D4D8',
borderWidth: 2,
borderColor: '#ffffff',
},
};
});
}
//
function initOptionsData() {
const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value;
functionCaseOptions.value.series.data = getPassRateData(functionalCount);
apiCaseOptions.value.series.data = getPassRateData(apiCaseCount);
scenarioCaseOptions.value.series.data = getPassRateData(apiScenarioCount);
}
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);
}
}
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{
name: t('report.detail.threshold'),
value: detail.value.passThreshold,
unit: '%',
icon: 'threshold',
},
{
name: t('report.passRate'),
value: detail.value.passRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.performCompletion'),
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 functionCasePassRate = computed(() => {
const apiCaseDetail = getSummaryDetail(detail.value.functionalCount || defaultCount);
return apiCaseDetail.successRate;
});
const apiCasePassRate = computed(() => {
const apiCaseDetail = getSummaryDetail(detail.value.apiCaseCount || defaultCount);
return apiCaseDetail.successRate;
});
const scenarioCasePassRate = computed(() => {
const apiScenarioDetail = getSummaryDetail(detail.value.apiScenarioCount || defaultCount);
return apiScenarioDetail.successRate;
});
const functionalCaseTotal = computed(() => getSummaryDetail(detail.value.functionalCount).caseTotal);
const apiCaseTotal = computed(() => getSummaryDetail(detail.value.apiCaseCount).caseTotal);
const scenarioCaseTotal = computed(() => getSummaryDetail(detail.value.apiScenarioCount).caseTotal);
const featureCaseTab = [
{
value: 'featureCase',
label: t('report.detail.featureCaseDetails'),
},
];
const scenarioCaseTab = [
{
value: 'scenarioCase',
label: t('report.detail.scenarioCaseDetails'),
},
];
const apiCaseTab = [
{
value: 'apiCase',
label: t('report.detail.apiCaseDetails'),
},
];
const activeTab = ref('bug');
const contentTabList = computed(() => {
const featureTab = functionalCaseTotal.value ? featureCaseTab : [];
const apiTab = apiCaseTotal.value ? apiCaseTab : [];
const scenarioTab = scenarioCaseTotal.value ? scenarioCaseTab : [];
return [
{
value: 'bug',
label: t('report.detail.bugDetails'),
},
...featureTab,
...apiTab,
...scenarioTab,
];
});
const cardCount = computed(() => {
const totalList = [functionalCaseTotal.value, apiCaseTotal.value, scenarioCaseTotal.value];
let count = 2;
totalList.forEach((item: number) => {
if (item > 0) {
count++;
}
});
return count;
});
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
richText.value.summary = detail.value.summary;
initOptionsData();
}
});
onMounted(async () => {
nextTick(() => {
const editorContent = document.querySelector('.editor-content');
useEventListener(editorContent, 'click', () => {
showButton.value = true;
});
});
});
function handleCancel() {
richText.value = { summary: detail.value.summary };
showButton.value = false;
}
function handleSummary(content: string) {
richText.value.summary = content;
}
</script>
<style scoped lang="less">
.block-title {
@apply mb-4 font-medium;
}
.analysis-wrapper {
@apply mb-4 grid items-center gap-4;
.analysis {
padding: 24px;
height: 250px;
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply rounded-xl bg-white;
.charts {
top: 36%;
right: 0;
bottom: 0;
left: 0;
z-index: 99;
margin: auto;
}
}
&[data-cards='2'],
&[data-cards='4'] {
grid-template-columns: repeat(2, 1fr);
}
&[data-cards='3'] {
grid-template-columns: repeat(3, 1fr);
}
// 523
&[data-cards='5'] {
grid-template-columns: repeat(6, 1fr);
& > .analysis:nth-child(1),
& > .analysis:nth-child(2) {
grid-column: span 3;
}
& > .analysis:nth-child(n + 3) {
grid-column: span 2;
}
}
}
</style>

View File

@ -1,185 +0,0 @@
<template>
<ReportHeader v-if="!props.isDrawer" :detail="detail" :share-id="shareId" is-group />
<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]">
<ExecuteAnalysis :detail="detail" />
</div>
</div>
<Summary
v-model:richText="richText"
:share-id="shareId"
:show-button="showButton"
:is-plan-group="true"
:detail="detail"
@update-summary="handleUpdateReportDetail"
@cancel="handleCancel"
@handle-summary="handleSummary"
/>
<MsCard simple auto-height auto-width>
<div class="mb-[16px] 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>
<ReportDetailTable :current-mode="currentMode" :report-id="detail.id" :share-id="shareId" />
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useEventListener } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue';
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/executeAnalysis.vue';
import ReportDetailTable from '@/views/test-plan/report/detail/component/reportDetailTable.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 } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type { PlanReportDetail, ReportMetricsItemModel } from '@/models/testPlan/testPlanReport';
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 reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{
name: t('report.detail.testPlanTotal'),
value: addCommasToNumber(detail.value.planCount),
unit: t('report.detail.number'),
icon: 'plan_total',
},
{
name: t('report.detail.testPlanCaseTotal'),
value: addCommasToNumber(detail.value.caseTotal),
unit: t('report.detail.number'),
icon: 'case_total',
},
{
name: t('report.passRate'),
value: detail.value.passRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.totalDefects'),
value: addCommasToNumber(detail.value.bugCount),
unit: t('report.detail.number'),
icon: 'bugTotal',
},
]);
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(content: string) {
richText.value.summary = content;
}
const currentMode = ref<string>('drawer');
const handleModeChange = (value: string | number | boolean) => {
currentMode.value = value as string;
};
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
richText.value.summary = detail.value.summary;
}
});
onMounted(async () => {
nextTick(() => {
const editorContent = document.querySelector('.editor-content');
useEventListener(editorContent, 'click', () => {
showButton.value = true;
});
});
});
</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

@ -1,38 +0,0 @@
<template>
<PlanGroupDetail v-if="props.isGroup" :detail-info="detail" @update-success="getDetail()" />
<PlanDetail v-else :detail-info="detail" @update-success="getDetail()" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
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 props = defineProps<{
isGroup: boolean;
reportId: string;
}>();
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
async function getDetail() {
try {
detail.value = await getReportDetail(props.reportId);
} catch (error) {
console.log(error);
}
}
onBeforeMount(() => {
getDetail();
});
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,175 @@
import { defaultCount } from '@/config/testPlan';
import { ApiOrScenarioCaseItem, FeatureCaseItem, ReportBugItem } from '@/models/testPlan/report';
import type { configItem } from '@/models/testPlan/testPlanReport';
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
export const commonDefaultConfig: configItem[] = [
{
id: ReportCardTypeEnum.SUMMARY,
value: ReportCardTypeEnum.SUMMARY,
label: 'report.detail.reportSummary',
system: true,
enableEdit: false,
},
{
id: ReportCardTypeEnum.BUG_DETAIL,
value: ReportCardTypeEnum.BUG_DETAIL,
label: 'report.detail.bugDetails',
system: true,
enableEdit: false,
},
{
id: ReportCardTypeEnum.FUNCTIONAL_DETAIL,
value: ReportCardTypeEnum.FUNCTIONAL_DETAIL,
label: 'report.detail.featureCaseDetails',
system: true,
enableEdit: false,
},
{
id: ReportCardTypeEnum.API_CASE_DETAIL,
value: ReportCardTypeEnum.API_CASE_DETAIL,
label: 'report.detail.apiCaseDetails',
system: true,
enableEdit: false,
},
{
id: ReportCardTypeEnum.SCENARIO_CASE_DETAIL,
value: ReportCardTypeEnum.SCENARIO_CASE_DETAIL,
label: 'report.detail.scenarioCaseDetails',
system: true,
enableEdit: false,
},
];
export const defaultCustomConfig: configItem = {
id: '',
value: ReportCardTypeEnum.CUSTOM_CARD,
label: 'report.detail.customDefaultCardName',
system: false,
enableEdit: false,
content: '',
};
// 独立报告默认配置
export const defaultSingleConfig: configItem[] = [...commonDefaultConfig];
// 集合报告默认配置
export const defaultGroupConfig: configItem[] = [
{
id: ReportCardTypeEnum.SUB_PLAN_DETAIL,
value: ReportCardTypeEnum.SUB_PLAN_DETAIL,
label: 'report.detail.subPlanDetails',
system: true,
enableEdit: false,
},
...commonDefaultConfig,
];
interface NamedItem {
name?: string;
title?: string;
testPlanName?: string;
}
export function createData<T extends NamedItem>(listItem: T): T[] {
const list = [];
for (let index = 0; index < 10; index++) {
const numIndex = index + 1;
const item = {
id: `Example_${index}`,
...listItem,
num: numIndex,
name: `${listItem.name}_${numIndex}`,
title: `${listItem.title}_${numIndex}`,
testPlanName: `${listItem.testPlanName}_${numIndex}`,
};
list.push(item);
}
return list;
}
// 示例数据
// 子计划报告
const subPlanList: PlanReportDetail = {
id: 'Example_738373617320062',
name: '子计划明细_示例数据',
testPlanName: '子计划明细_示例数据',
createTime: 1719374179322,
startTime: 0,
endTime: 0,
summary: '',
caseTotal: 1,
passThreshold: 100.0,
passRate: 0.0,
executeRate: 0,
bugCount: 0,
planCount: 0,
executeCount: defaultCount,
functionalCount: defaultCount,
apiCaseCount: defaultCount,
apiScenarioCount: defaultCount,
passCountOfPlan: 0,
failCountOfPlan: 0,
functionalBugCount: 0,
apiBugCount: 0,
scenarioBugCount: 0,
resultStatus: 'SUCCESS',
};
// 功能用例明细
const functionalList: FeatureCaseItem = {
id: 'Example_738373617320062',
num: 1,
name: '用例明细_示例数据',
moduleName: '/未规划模块',
priority: 'P1',
executeResult: 'SUCCESS',
executeUserName: '',
bugCount: 0,
};
// 缺陷明细
const bugList: ReportBugItem = {
id: 'Example_738373617320062',
num: 1,
title: '缺陷明细_示例数据',
status: '新建',
handleUserName: 'admin',
relationCaseCount: 0,
};
// 接口明细
const apiCaseList: ApiOrScenarioCaseItem = {
id: 'Example_738373617320062',
num: 1,
name: '接口明细_示例数据',
moduleName: '/未规划模块',
priority: 'P0',
executeResult: 'SUCCESS',
executeUser: 'admin',
bugCount: 0,
reportId: '718255970852864',
projectId: '718255970852864',
};
// 场景明细
const scenarioCaseList: ApiOrScenarioCaseItem = {
id: 'Example_738373617320062',
num: 1,
name: '场景明细_示例数据',
moduleName: '/未规划模块',
priority: 'P2',
executeResult: 'SUCCESS',
executeUser: '社恐程序员',
bugCount: 0,
reportId: '718255970852864',
projectId: '718255970852864',
};
export const detailTableExample: Record<string, any> = {
[ReportCardTypeEnum.SUB_PLAN_DETAIL]: createData<PlanReportDetail>(subPlanList),
[ReportCardTypeEnum.FUNCTIONAL_DETAIL]: createData<FeatureCaseItem>(functionalList),
[ReportCardTypeEnum.BUG_DETAIL]: createData<ReportBugItem>(bugList),
[ReportCardTypeEnum.API_CASE_DETAIL]: createData<ApiOrScenarioCaseItem>(apiCaseList),
[ReportCardTypeEnum.SCENARIO_CASE_DETAIL]: createData<ApiOrScenarioCaseItem>(scenarioCaseList),
};
export default {};

View File

@ -26,9 +26,13 @@
v-model:visible="reportVisible"
:report-id="apiReportId"
do-not-show-share
:is-scenario="props.activeTab === 'scenarioCase'"
:report-detail="props.activeTab === 'scenarioCase' ? reportScenarioDetail : reportCaseDetail"
:get-report-step-detail="props.activeTab === 'scenarioCase' ? reportStepDetail : reportCaseStepDetail"
:is-scenario="props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL"
:report-detail="
props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL ? reportScenarioDetail : reportCaseDetail
"
:get-report-step-detail="
props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL ? reportStepDetail : reportCaseStepDetail
"
/>
</template>
@ -56,15 +60,18 @@
import { ReportEnum } from '@/enums/reportEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { casePriorityOptions, lastReportStatusListOptions } from '@/views/api-test/components/config';
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
const { openNewPage } = useOpenNewPage();
const props = defineProps<{
reportId: string;
shareId?: string;
activeTab: string;
activeType: ReportCardTypeEnum;
isPreview?: boolean;
}>();
const columns: MsTableColumn = [
@ -128,13 +135,6 @@
},
];
const getReportApiList = () => {
if (props.activeTab === 'apiCase') {
return getApiPage;
}
return getScenarioPage;
};
const useApiTable = useTable(getApiPage, {
scroll: { x: '100%' },
columns,
@ -149,7 +149,7 @@
});
const currentCaseTable = computed(() => {
return props.activeTab === 'apiCase' ? useApiTable : useScenarioTable;
return props.activeType === ReportCardTypeEnum.API_CASE_DETAIL ? useApiTable : useScenarioTable;
});
async function loadCaseList() {
@ -170,7 +170,7 @@
//
function toDetail(record: PlanDetailApiCaseItem) {
if (props.activeTab === 'scenarioCase') {
if (props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL) {
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
id: record.id,
pId: record.projectId,
@ -183,16 +183,13 @@
}
}
watch(
() => props.activeTab,
() => {
watchEffect(() => {
if (props.reportId && props.activeType && props.isPreview) {
currentCaseTable.value.resetFilterParams();
currentCaseTable.value.resetPagination();
loadCaseList();
} else {
currentCaseTable.value.propsRes.value.data = detailTableExample[props.activeType];
}
);
onMounted(() => {
loadCaseList();
});
</script>

View File

@ -3,17 +3,23 @@
</template>
<script setup lang="ts">
import { onBeforeMount } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
import { useI18n } from '@/hooks/useI18n';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
const { t } = useI18n();
const props = defineProps<{
reportId: string;
shareId?: string;
isPreview?: boolean;
}>();
const columns: MsTableColumn = [
@ -73,8 +79,10 @@
}
watchEffect(() => {
if (props.reportId) {
if (props.reportId && props.isPreview) {
loadCaseList();
} else {
propsRes.value.data = detailTableExample[ReportCardTypeEnum.BUG_DETAIL];
}
});
</script>

View File

@ -8,31 +8,60 @@
</template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.executeResult" />
<MsButton class="ml-[8px]" :disabled="!props.isPreview" @click="openExecuteHistory(record)">{{
t('common.detail')
}}</MsButton>
</template>
</MsBaseTable>
<MsDrawer
v-model:visible="showDetailVisible"
:title="t('ms.case.associate.title')"
:width="1200"
:footer="false"
no-content-padding
unmount-on-close
>
<!-- TODO 等待联调 后台没出接口 -->
<ExecutionHistory
:extra-params="{
caseId: '',
id: '',
testPlanId: '',
}"
:load-list-fun="executeHistory"
/>
</MsDrawer>
</template>
<script setup lang="ts">
import { onBeforeMount } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import ExecutionHistory from '@/views/test-plan/testPlan/detail/featureCase/detail/executionHistory/index.vue';
import { getReportFeatureCaseList, getReportShareFeatureCaseList } from '@/api/modules/test-plan/report';
import { executeHistory } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { FeatureCaseItem } from '@/models/testPlan/report';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
const props = defineProps<{
reportId: string;
shareId?: string;
activeTab: string;
isPreview?: boolean;
}>();
const { t } = useI18n();
const columns: MsTableColumn = [
{
title: 'ID',
@ -58,6 +87,18 @@
},
width: 180,
},
{
title: 'common.executionResult',
dataIndex: 'executeResult',
slotName: 'lastExecResult',
filterConfig: {
valueKey: 'key',
labelKey: 'statusText',
options: Object.values(executionResultMap),
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
},
width: 150,
},
{
title: 'common.belongModule',
dataIndex: 'moduleName',
@ -71,18 +112,7 @@
slotName: 'caseLevel',
width: 120,
},
{
title: 'common.executionResult',
dataIndex: 'executeResult',
slotName: 'lastExecResult',
filterConfig: {
valueKey: 'key',
labelKey: 'statusText',
options: Object.values(executionResultMap),
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
},
width: 150,
},
{
title: 'testPlan.featureCase.executor',
dataIndex: 'executeUser',
@ -111,8 +141,19 @@
}
watchEffect(() => {
if (props.reportId) {
if (props.reportId && props.isPreview) {
loadCaseList();
} else {
propsRes.value.data = detailTableExample[ReportCardTypeEnum.FUNCTIONAL_DETAIL];
}
});
const showDetailVisible = ref<boolean>(false);
const detailRecord = ref();
function openExecuteHistory(record: FeatureCaseItem) {
detailRecord.value = record;
showDetailVisible.value = true;
}
</script>

View File

@ -23,7 +23,7 @@
<ExecutionStatus :status="filterContent.value" />
</template>
<template #operation="{ record }">
<MsButton class="!mx-0" :disabled="record.deleted" @click="openReport(record)">{{
<MsButton class="!mx-0" :disabled="record.deleted || !props.isPreview" @click="openReport(record)">{{
t('report.detail.testPlanGroup.viewReport')
}}</MsButton>
</template>
@ -49,6 +49,9 @@
import { PlanReportStatus } from '@/enums/reportEnum';
import { RouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
const { openNewPage } = useOpenNewPage();
@ -57,9 +60,13 @@
const props = defineProps<{
reportId: string;
shareId?: string;
currentMode: string;
isPreview?: boolean;
}>();
const innerCurrentMode = defineModel<string>('currentMode', {
default: 'drawer',
});
const statusResultOptions = computed(() => {
return Object.keys(PlanReportStatus).map((key) => {
return {
@ -132,8 +139,10 @@
}
watchEffect(() => {
if (props.reportId) {
if (props.reportId && props.isPreview) {
loadReportDetailList();
} else {
propsRes.value.data = detailTableExample[ReportCardTypeEnum.SUB_PLAN_DETAIL];
}
});
@ -143,7 +152,7 @@
function openReport(record: PlanReportDetail) {
independentReportId.value = record.id;
if (props.currentMode === 'drawer') {
if (innerCurrentMode.value === 'drawer') {
reportVisible.value = true;
} else {
openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, {

View File

@ -17,7 +17,7 @@
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 PlanDetailHeaderRight from '@/views/test-plan/report/detail/component/system-card/planDetailHeaderRight.vue';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';

View File

@ -1,19 +1,16 @@
<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'}`"
>
<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="ReportPlanPreviewImageUrl"
class="mt-[8px] w-full"
:editable="!!shareId"
:editable="props.canEdit"
@click="handleClick"
/>
<MsFormItemSub
v-if="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && props.showButton"
v-if="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && props.showButton && props.canEdit"
:text="t('report.detail.oneClickSummary')"
:show-fill-icon="true"
@fill="handleSummary"
@ -21,25 +18,24 @@
</div>
<div
v-show="props.showButton && hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId"
v-show="props.showButton && hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && props.canEdit"
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 { ReportPlanPreviewImageUrl } from '@/api/requrls/test-plan/report';
import useDoubleClick from '@/hooks/useDoubleClick';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
@ -54,11 +50,13 @@
showButton: boolean;
isPlanGroup: boolean;
detail: PlanReportDetail;
canEdit: boolean;
}>();
const emit = defineEmits<{
(e: 'updateSummary'): void;
(e: 'cancel'): void;
(e: 'dblclick'): void;
(e: 'handleSummary', content: string): void;
}>();
@ -128,6 +126,11 @@
function handleSummary() {
emit('handleSummary', summaryContent.value);
}
function emitDoubleClick() {
emit('dblclick');
}
const { handleClick } = useDoubleClick(emitDoubleClick);
</script>
<style scoped></style>

View File

@ -0,0 +1,29 @@
<template>
<a-trigger
unmount-on-close
trigger="hover"
:disabled="props.isPreview"
:popup-translate="props.popupTranslate || [100, -100]"
>
<slot></slot>
<template #content>
<div class="arco-table-filters-content px-[8px] py-[4px]">{{ t('report.detail.systemInternalTooltip') }}</div>
</template>
</a-trigger>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { TriggerPopupTranslate } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
isPreview?: boolean;
popupTranslate?: TriggerPopupTranslate | undefined;
}>();
</script>
<style scoped></style>

View File

@ -0,0 +1,663 @@
<template>
<ReportHeader v-if="!props.isDrawer && props.isPreview" :detail="detail" :share-id="shareId" :is-group="false" />
<div class="analysis-wrapper" :data-cards="cardCount">
<SystemTrigger :is-preview="props.isPreview">
<div :class="`${getAnalysisHover} 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>
</SystemTrigger>
<SystemTrigger :is-preview="props.isPreview">
<div :class="`${getAnalysisHover} analysis min-w-[410px]`">
<ExecuteAnalysis :detail="detail" />
</div>
<template #content>
<div class="arco-table-filters-content px-[8px] py-[4px]">{{ t('report.detail.systemInternalTooltip') }}</div>
</template>
</SystemTrigger>
<SystemTrigger :is-preview="props.isPreview">
<div v-if="functionalCaseTotal" :class="`${getAnalysisHover} analysis min-w-[330px]`">
<div class="block-title">{{ t('report.detail.useCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<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">
<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-[80px] text-[var(--color-text-1)]">{{ functionCasePassRate }} </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)]">{{
functionCasePassRate
}}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="functionCaseOptions" />
</div>
</div>
</div>
</div>
</SystemTrigger>
<SystemTrigger :is-preview="props.isPreview">
<div v-if="apiCaseTotal" :class="`${getAnalysisHover} analysis min-w-[330px]`">
<div class="block-title">{{ t('report.detail.apiUseCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<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">
<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-[80px] text-[var(--color-text-1)]">{{ apiCasePassRate }} </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)]">{{ apiCasePassRate }}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="apiCaseOptions" />
</div>
</div>
</div>
</div>
<template #content>
<div class="arco-table-filters-content px-[8px] py-[4px]">{{ t('report.detail.systemInternalTooltip') }}</div>
</template>
</SystemTrigger>
<SystemTrigger :is-preview="props.isPreview">
<div v-if="scenarioCaseTotal" :class="`${getAnalysisHover} analysis min-w-[330px]`">
<div class="block-title">{{ t('report.detail.scenarioUseCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<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">
<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-[80px] text-[var(--color-text-1)]">{{ scenarioCasePassRate }} </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)]">{{
scenarioCasePassRate
}}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="scenarioCaseOptions" />
</div>
</div>
</div>
</div>
</SystemTrigger>
</div>
<div :class="`${props.isPreview ? 'mt-[16px]' : 'mt-[24px]'} drag-container`">
<VueDraggable v-model="innerCardList" :disabled="props.isPreview" group="report">
<div
v-for="(item, index) of innerCardList"
v-show="showItem(item)"
:key="item.id"
:class="`${props.isPreview ? 'mt-[16px]' : 'hover-card mt-[24px]'} card-item`"
>
<div v-if="!props.isPreview" class="action">
<div class="actionList">
<a-tooltip :content="t('system.orgTemplate.toTop')">
<MsIcon
type="icon-icon_up_outlined"
size="16"
:class="getColor(index, 'top')"
@click="moveCard(item, 'top')"
/>
</a-tooltip>
<a-divider direction="vertical" class="!m-0 !mx-2" />
<a-tooltip :content="t('system.orgTemplate.toBottom')">
<MsIcon
:class="getColor(index, 'bottom')"
type="icon-icon_down_outlined"
size="16"
@click="moveCard(item, 'bottom')"
/>
</a-tooltip>
<a-divider direction="vertical" class="!m-0 !mx-2" />
<a-tooltip v-if="allowEdit(item.value)" :content="t('common.edit')">
<MsIcon type="icon-icon_edit_outlined" size="16" @click="editField(item)" />
</a-tooltip>
<a-divider v-if="allowEdit(item.value)" direction="vertical" class="!m-0 !mx-2" />
<a-tooltip :content="t('common.delete')">
<MsIcon type="icon-icon_delete-trash_outlined" size="16" @click="deleteCard(item)" />
</a-tooltip>
</div>
</div>
<MsCard simple auto-height auto-width>
<div v-if="item.value !== ReportCardTypeEnum.CUSTOM_CARD" class="mb-[8px] font-medium">
{{ t(item.label) }}
</div>
<ReportDetailTable
v-if="item.value === ReportCardTypeEnum.SUB_PLAN_DETAIL"
v-model:current-mode="currentMode"
:report-id="detail.id"
:share-id="shareId"
:is-preview="props.isPreview"
/>
<Summary
v-else-if="item.value === ReportCardTypeEnum.SUMMARY"
v-model:richText="richText"
:share-id="shareId"
:can-edit="item.enableEdit"
:show-button="showButton"
:is-plan-group="props.isGroup"
:detail="detail"
@update-summary="handleUpdateReportDetail"
@cancel="() => handleCancel(item)"
@handle-summary="handleSummary"
@dblclick="handleDoubleClick(item)"
/>
<BugTable
v-else-if="item.value === ReportCardTypeEnum.BUG_DETAIL"
:report-id="detail.id"
:share-id="shareId"
:is-preview="props.isPreview"
/>
<FeatureCaseTable
v-else-if="item.value === ReportCardTypeEnum.FUNCTIONAL_DETAIL"
:report-id="detail.id"
:share-id="shareId"
:is-preview="props.isPreview"
/>
<ApiAndScenarioTable
v-else-if="
item.value === ReportCardTypeEnum.API_CASE_DETAIL ||
item.value === ReportCardTypeEnum.SCENARIO_CASE_DETAIL
"
:report-id="detail.id"
:share-id="shareId"
:active-type="item.value"
:is-preview="props.isPreview"
/>
<CustomRichText
v-else-if="item.value === ReportCardTypeEnum.CUSTOM_CARD"
:can-edit="item.enableEdit"
:share-id="shareId"
:current-id="item.id"
:custom-form="{
content: item.content,
label: t(item.label),
richTextTmpFileIds: [],
}"
@update-custom="(formValue:customValueForm)=>updateCustom(formValue,item)"
@dblclick="handleDoubleClick(item)"
/>
</MsCard>
</div>
</VueDraggable>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useEventListener } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import { VueDraggable } from 'vue-draggable-plus';
import MsChart from '@/components/pure/chart/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
import CustomRichText from '@/views/test-plan/report/detail/component/custom-card/customRichText.vue';
import ApiAndScenarioTable from '@/views/test-plan/report/detail/component/system-card/apiAndScenarioTable.vue';
import BugTable from '@/views/test-plan/report/detail/component/system-card/bugTable.vue';
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/system-card/executeAnalysis.vue';
import FeatureCaseTable from '@/views/test-plan/report/detail/component/system-card/featureCaseTable.vue';
import ReportDetailTable from '@/views/test-plan/report/detail/component/system-card/reportDetailTable.vue';
import ReportHeader from '@/views/test-plan/report/detail/component/system-card/reportHeader.vue';
import ReportMetricsItem from '@/views/test-plan/report/detail/component/system-card/ReportMetricsItem.vue';
import Summary from '@/views/test-plan/report/detail/component/system-card/summary.vue';
import SystemTrigger from '@/views/test-plan/report/detail/component/system-card/systemTrigger.vue';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { commonConfig, defaultCount, defaultReportDetail, seriesConfig, statusConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type {
configItem,
countDetail,
PlanReportDetail,
ReportMetricsItemModel,
StatusListType,
} from '@/models/testPlan/testPlanReport';
import { customValueForm } from '@/models/testPlan/testPlanReport';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { getSummaryDetail } from '@/views/test-plan/report/utils';
const { t } = useI18n();
const route = useRoute();
const props = defineProps<{
detailInfo: PlanReportDetail;
isDrawer?: boolean;
isGroup?: boolean;
isPreview?: boolean;
}>();
const emit = defineEmits<{
(e: 'updateSuccess'): void;
(e: 'updateSuccess'): void;
(e: 'updateCustom', item: configItem): void;
}>();
const innerCardList = defineModel<configItem[]>('cardList', {
default: [],
});
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const showButton = ref<boolean>(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
const isError = ref(false);
const reportForm = ref({
reportName: '',
});
function inputHandler(value: string) {
if (value.trim().length === 0) {
isError.value = true;
}
isError.value = false;
}
const getAnalysisHover = computed(() => (props.isPreview ? '' : 'hover-analysis cursor-not-allowed'));
/**
* 分享share
*/
const shareId = ref<string>(route.query.shareId as string);
//
const functionCaseOptions = ref({
...commonConfig,
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
],
},
});
//
const apiCaseOptions = ref({
...commonConfig,
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
],
},
});
//
const scenarioCaseOptions = ref({
...commonConfig,
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
],
},
});
//
function getPassRateData(caseDetailCount: countDetail) {
const caseCountDetail = caseDetailCount || defaultCount;
const passRateData = statusConfig.filter((item) => ['success'].includes(item.value));
const { success } = caseCountDetail;
const valueList = success ? statusConfig : passRateData;
return valueList.map((item: StatusListType) => {
return {
value: caseCountDetail[item.value] || 0,
name: t(item.label),
itemStyle: {
color: success ? item.color : '#D4D4D8',
borderWidth: 2,
borderColor: '#ffffff',
},
};
});
}
//
function initOptionsData() {
const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value;
functionCaseOptions.value.series.data = getPassRateData(functionalCount);
apiCaseOptions.value.series.data = getPassRateData(apiCaseCount);
scenarioCaseOptions.value.series.data = getPassRateData(apiScenarioCount);
}
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);
}
}
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{
name: t('report.detail.threshold'),
value: detail.value.passThreshold,
unit: '%',
icon: 'threshold',
},
{
name: t('report.passRate'),
value: detail.value.passRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.performCompletion'),
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 functionCasePassRate = computed(() => {
const apiCaseDetail = getSummaryDetail(detail.value.functionalCount || defaultCount);
return apiCaseDetail.successRate;
});
const apiCasePassRate = computed(() => {
const apiCaseDetail = getSummaryDetail(detail.value.apiCaseCount || defaultCount);
return apiCaseDetail.successRate;
});
const scenarioCasePassRate = computed(() => {
const apiScenarioDetail = getSummaryDetail(detail.value.apiScenarioCount || defaultCount);
return apiScenarioDetail.successRate;
});
const functionalCaseTotal = computed(() => getSummaryDetail(detail.value.functionalCount).caseTotal);
const apiCaseTotal = computed(() => getSummaryDetail(detail.value.apiCaseCount).caseTotal);
const scenarioCaseTotal = computed(() => getSummaryDetail(detail.value.apiScenarioCount).caseTotal);
function showItem(item: configItem) {
switch (item.value) {
case ReportCardTypeEnum.FUNCTIONAL_DETAIL:
return functionalCaseTotal.value > 0;
case ReportCardTypeEnum.API_CASE_DETAIL:
return apiCaseTotal.value > 0;
case ReportCardTypeEnum.SCENARIO_CASE_DETAIL:
return scenarioCaseTotal.value > 0;
default:
return true;
}
}
const cardCount = computed(() => {
const totalList = [functionalCaseTotal.value, apiCaseTotal.value, scenarioCaseTotal.value];
let count = 2;
totalList.forEach((item: number) => {
if (item > 0) {
count++;
}
});
return count;
});
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
richText.value.summary = detail.value.summary;
reportForm.value.reportName = detail.value.name;
initOptionsData();
}
});
onMounted(async () => {
nextTick(() => {
const editorContent = document.querySelector('.editor-content');
useEventListener(editorContent, 'click', () => {
showButton.value = true;
});
});
});
function handleCancel(cardItem: configItem) {
richText.value = { summary: detail.value.summary };
showButton.value = false;
cardItem.enableEdit = false;
}
function handleSummary(content: string) {
richText.value.summary = content;
}
const currentMode = ref<string>('drawer');
function getColor(index: number, type: string) {
if (type === 'top' && index === 0) {
return ['text-[rgb(var(--primary-3))]'];
}
if (type === 'bottom' && index === innerCardList.value.length - 1) {
return ['text-[rgb(var(--primary-3))]'];
}
}
const allowEditType = [ReportCardTypeEnum.SUMMARY, ReportCardTypeEnum.CUSTOM_CARD];
function allowEdit(value: ReportCardTypeEnum) {
return allowEditType.includes(value);
}
//
function moveCard(cardItem: configItem, type: string) {
const moveIndex = innerCardList.value.findIndex((item: any) => item.id === cardItem.id);
if (type === 'top') {
if (moveIndex === 0) {
return;
}
innerCardList.value.splice(moveIndex, 1);
innerCardList.value.splice(moveIndex - 1, 0, cardItem);
} else {
if (moveIndex === innerCardList.value.length - 1) {
return;
}
innerCardList.value.splice(moveIndex, 1);
innerCardList.value.splice(moveIndex + 1, 0, cardItem);
}
}
//
const deleteCard = (cardItem: configItem) => {
innerCardList.value = innerCardList.value.filter((item) => item.id !== cardItem.id);
};
//
function editField(cardItem: configItem) {
if (allowEditType.includes(cardItem.value)) {
cardItem.enableEdit = !cardItem.enableEdit;
}
if (cardItem.value === ReportCardTypeEnum.SUMMARY) {
showButton.value = true;
}
}
function handleDoubleClick(cardItem: configItem) {
editField(cardItem);
}
function updateCustom(formValue: customValueForm, currentItem: configItem) {
const newCurrentItem = {
...currentItem,
...formValue,
};
innerCardList.value = innerCardList.value.map((item) => {
if (item.id === currentItem.id) {
return {
...item,
...formValue,
enableEdit: false,
};
}
return item;
});
emit('updateCustom', newCurrentItem);
}
</script>
<style scoped lang="less">
.report-name {
padding: 0 16px;
height: 56px;
@apply flex items-center justify-between border-b bg-white;
}
.block-title {
@apply mb-4 font-medium;
}
.config-right-container {
padding: 16px;
width: calc(100% - 300px);
background: var(--color-bg-3);
}
.analysis-wrapper {
@apply mb-4 grid items-center gap-4;
.analysis {
padding: 24px;
height: 250px;
border: 1px solid transparent;
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply rounded-xl bg-white;
.charts {
top: 36%;
right: 0;
bottom: 0;
left: 0;
z-index: 99;
margin: auto;
}
}
&[data-cards='2'],
&[data-cards='4'] {
grid-template-columns: repeat(2, 1fr);
}
&[data-cards='3'] {
grid-template-columns: repeat(3, 1fr);
}
// 523
&[data-cards='5'] {
grid-template-columns: repeat(6, 1fr);
& > .analysis:nth-child(1),
& > .analysis:nth-child(2) {
grid-column: span 3;
}
& > .analysis:nth-child(n + 3) {
grid-column: span 2;
}
}
}
.hover-analysis {
&:hover {
border: 1px solid rgb(var(--primary-5));
}
}
.drag-container {
.card-item {
position: relative;
border: 1px solid transparent;
border-radius: 12px;
.action {
position: absolute;
top: -14px;
left: 50%;
transform: translateX(-50%);
z-index: 9 !important;
background: white;
opacity: 0;
@apply flex items-center justify-end;
.actionList {
padding: 4px;
border-radius: 4px;
@apply flex items-center justify-center;
}
}
&:hover > .action {
opacity: 1;
}
&:hover > .action > .actionList {
color: rgb(var(--primary-5));
box-shadow: 0 4px 10px -1px rgba(100 100 102/ 15%);
}
}
}
.hover-card {
&:hover {
border: 1px solid rgb(var(--primary-5));
background: var(--color-text-n9);
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<configDetail :is-group="isGroup" :detail-info="detail" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import configDetail from './component/config.vue';
import { getPlanPassRate } from '@/api/modules/test-plan/testPlan';
import type { detailCountKey, PlanReportDetail } from '@/models/testPlan/testPlanReport';
const route = useRoute();
const detail = ref<PlanReportDetail>({
id: '',
name: '',
startTime: 0,
createTime: 0, //
endTime: 0,
summary: '',
passThreshold: 100, //
passRate: 100, //
executeRate: 100, //
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,
},
apiCaseCount: {
success: 0,
error: 0,
fakeError: 0,
block: 0,
pending: 0,
},
apiScenarioCount: {
success: 0,
error: 0,
fakeError: 0,
block: 0,
pending: 0,
},
planCount: 10,
passCountOfPlan: 0,
failCountOfPlan: 0,
functionalBugCount: 0,
apiBugCount: 0,
scenarioBugCount: 0,
testPlanName: '',
});
const isGroup = computed(() => route.query.type === 'GROUP');
async function getStatistics() {
detail.value.caseTotal = 0;
try {
const selectedPlanIds = [route.query.id];
const result = await getPlanPassRate(selectedPlanIds as string[]);
const [countDetail] = result;
const { apiCaseCount, apiScenarioCount, functionalCaseCount } = countDetail;
const totalCase: { key: detailCountKey; count: number }[] = [
{ key: 'apiCaseCount', count: apiCaseCount },
{ key: 'apiScenarioCount', count: apiScenarioCount },
{ key: 'functionalCount', count: functionalCaseCount },
];
totalCase.forEach((item: { key: detailCountKey; count: number }) => {
if (item.count > 0) {
detail.value.caseTotal += 10;
detail.value.executeCount.success += 10;
detail.value[item.key].success = 10;
}
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
getStatistics();
});
</script>
<style scoped lang="less"></style>

View File

@ -1,20 +1,21 @@
<template>
<PlanGroupDetail v-if="isGroup" :detail-info="detail" @update-success="getDetail()" />
<PlanDetail v-else :detail-info="detail" @update-success="getDetail()" />
<ViewReport v-model:card-list="cardItemList" :detail-info="detail" :is-group="isGroup" is-preview />
</template>
<script setup lang="ts">
// TODO
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
import PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
import { getReportDetail } from '@/api/modules/test-plan/report';
import { defaultReportDetail } from '@/config/testPlan';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { defaultGroupConfig, defaultSingleConfig } from '@/views/test-plan/report/detail/component/reportConfig';
const route = useRoute();
const reportId = ref<string>(route.query.id as string);
@ -23,7 +24,10 @@
const isGroup = computed(() => route.query.type === 'GROUP');
const cardItemList = ref<configItem[]>([]);
async function getDetail() {
cardItemList.value = isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
try {
detail.value = await getReportDetail(reportId.value);
} catch (error) {

View File

@ -1,21 +1,22 @@
<template>
<PlanGroupDetail v-if="isGroup" :detail-info="detail" />
<PlanDetail v-else :detail-info="detail" />
<ViewReport v-model:card-list="cardItemList" :detail-info="detail" :is-group="isGroup" is-preview />
</template>
<script setup lang="ts">
// TODO
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
import PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
import { getReportDetail, planGetShareHref } from '@/api/modules/test-plan/report';
import { defaultReportDetail } from '@/config/testPlan';
import { NOT_FOUND_RESOURCE } from '@/router/constants';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { defaultGroupConfig, defaultSingleConfig } from '@/views/test-plan/report/detail/component/reportConfig';
const route = useRoute();
const router = useRouter();
@ -23,7 +24,9 @@
const isGroup = computed(() => route.query.type === 'GROUP');
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
const cardItemList = ref<configItem[]>([]);
async function getShareDetail() {
cardItemList.value = isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
try {
const hrefShareDetail = await planGetShareHref(route.query.shareId as string);
reportId.value = hrefShareDetail.reportId;

View File

@ -26,7 +26,7 @@ export default {
'report.passRateTip': 'Pass rate: successful cases in the plan / cases in the plan * 100%',
'report.detail.reportSummary': 'Report summary',
'report.detail.bugDetails': 'Bug details',
'report.detail.featureCaseDetails': 'Feature case details',
'report.detail.featureCaseDetails': 'Case details',
'report.detail.executionAnalysis': 'Execution Analysis',
'report.detail.threshold': 'Pass threshold',
'report.detail.performCompletion': 'Perform completion',
@ -36,8 +36,8 @@ export default {
'report.detail.scenarioUseCaseAnalysis': 'Scenario use case analysis',
'report.detail.number': 'number',
'report.detail.level': 'level',
'report.detail.apiCaseDetails': 'Api use case details',
'report.detail.scenarioCaseDetails': 'Scenario use case details',
'report.detail.apiCaseDetails': 'Api details',
'report.detail.scenarioCaseDetails': 'Scenario details',
'report.detail.oneClickSummary': 'One click report summary',
'report.detail.testPlanTotal': 'Total plan',
'report.detail.testPlanCaseTotal': 'Total use cases',
@ -46,4 +46,13 @@ export default {
'report.detail.testPlanGroup.viewReport': 'View Report',
'report.detail.testReport': 'Planning report',
'report.detail.testPlanGroupReport': 'Planning groups report',
'report.detail.customFieldTooltip': 'Click Add card; Custom can add up to 10; Card support sort',
'report.detail.customButton': 'Custom',
'report.detail.customDefaultCardName': 'Custom title',
'report.detail.subPlanDetails': 'Sub-plan report details',
'report.detail.customTitlePlaceHolder': 'Please enter a custom title',
'report.detail.baseField': 'Base field',
'report.detail.customMaxNumber': 'A maximum of 10 can be added',
'report.detail.enterReportNamePlaceHolder': 'Please enter a report name',
'report.detail.systemInternalTooltip': 'System built-in, not editable',
};

View File

@ -26,7 +26,7 @@ export default {
'report.completed': '已完成',
'report.detail.reportSummary': '报告总结',
'report.detail.bugDetails': '缺陷明细',
'report.detail.featureCaseDetails': '功能用例明细',
'report.detail.featureCaseDetails': '用例明细',
'report.detail.executionAnalysis': '执行分析',
'report.detail.threshold': '通过阈值',
'report.detail.performCompletion': '执行完成率',
@ -36,8 +36,8 @@ export default {
'report.detail.scenarioUseCaseAnalysis': '场景用例分析',
'report.detail.number': '个',
'report.detail.level': '等级',
'report.detail.apiCaseDetails': '接口用例明细',
'report.detail.scenarioCaseDetails': '场景用例明细',
'report.detail.apiCaseDetails': '接口明细',
'report.detail.scenarioCaseDetails': '场景明细',
'report.detail.oneClickSummary': '一键填写报告总结',
'report.detail.testPlanTotal': '计划总数',
'report.detail.testPlanCaseTotal': '用例总数',
@ -46,4 +46,13 @@ export default {
'report.detail.testPlanGroup.viewReport': '查看报告',
'report.detail.testReport': '计划报告',
'report.detail.testPlanGroupReport': '计划组报告',
'report.detail.customFieldTooltip': '点击添加卡片;自定义最多可添加 10 个;卡片支持排序',
'report.detail.customButton': '自定义',
'report.detail.customDefaultCardName': '自定义标题',
'report.detail.subPlanDetails': '子计划报告明细',
'report.detail.customTitlePlaceHolder': '请输入自定义标题',
'report.detail.baseField': '基础字段',
'report.detail.customMaxNumber': '最多可添加10个',
'report.detail.enterReportNamePlaceHolder': '请输入报告名称',
'report.detail.systemInternalTooltip': '系统内置,不可编辑',
};

View File

@ -0,0 +1,81 @@
<template>
<div
v-if="record.type === testPlanTypeEnum.GROUP || record.integrated"
class="mr-2 flex items-center"
@click="expandHandler"
>
<MsIcon
type="icon-icon_split_turn-down_arrow"
class="arrowIcon mr-1 cursor-pointer text-[16px]"
:class="getIconClass"
/>
<span :class="getIconClass">{{ record.childrenCount || (record.children || []).length || 0 }}</span>
</div>
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN || !record.integrated" :class="`one-line-text ${hasIndent}`">
<MsButton type="text" @click="handleAction">
<a-tooltip :content="content">
<span>{{ record[props.numKey || 'num'] }}</span>
</a-tooltip>
</MsButton>
</div>
<a-tooltip v-else :content="content">
<div :class="`one-line-text ${hasIndent}`">{{ record[props.numKey || 'num'] }}</div>
</a-tooltip>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { hasAnyPermission } from '@/utils/permission';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const props = defineProps<{
record: Record<string, any>;
numKey?: string;
idKey?: string;
permission?: string[];
}>();
const emit = defineEmits<{
(e: 'expand'): void;
(e: 'action'): void;
}>();
const innerExpandedKeys = defineModel<string[]>('expandedKeys', { default: [] });
const getIconClass = computed(() => {
if (hasAnyPermission(props.permission || [])) {
return innerExpandedKeys.value.includes(props.record[props.idKey || 'id'])
? 'text-[rgb(var(--primary-5))]'
: 'text-[var(--color-text-4)]';
}
return '';
});
const content = computed(() => {
const key = props.numKey || 'num';
return typeof props.record[key] === 'string' ? props.record[key] : props.record[key].toString();
});
const hasIndent = computed(() =>
(props.record.type === testPlanTypeEnum.TEST_PLAN && props.record.groupId && props.record.groupId !== 'NONE') ||
(!props.record.integrated && props.record.parentId)
? 'pl-[36px]'
: ''
);
function expandHandler() {
emit('expand');
}
function handleAction() {
if (hasAnyPermission(props.permission || [])) {
emit('action');
}
}
</script>
<style scoped lang="less"></style>

View File

@ -97,29 +97,12 @@
</template> -->
<template #num="{ record }">
<div class="flex items-center">
<div
v-if="record.type === testPlanTypeEnum.GROUP"
class="mr-2 flex items-center"
@click="expandHandler(record)"
>
<MsIcon
type="icon-icon_split_turn-down_arrow"
class="arrowIcon mr-1 cursor-pointer text-[16px]"
:class="getIconClass(record)"
<PlanExpandRow
v-model:expanded-keys="expandedKeys"
:record="record"
@action="openDetail(record.id)"
@expand="expandHandler(record)"
/>
<span :class="getIconClass(record)">{{ record.childrenCount || 0 }}</span>
</div>
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN" :class="`one-line-text ${hasIndent(record)}`">
<MsButton type="text" @click="openDetail(record.id)"
><a-tooltip :content="record.num.toString()"
><span>{{ record.num }}</span></a-tooltip
></MsButton
>
</div>
<a-tooltip v-else :content="record.num.toString()">
<div :class="`one-line-text ${hasIndent(record)}`">{{ record.num }}</div>
</a-tooltip>
<a-tooltip position="right" :disabled="!getSchedule(record.id)" :mouse-enter-delay="300">
<MsTag
v-if="getSchedule(record.id)"
@ -186,13 +169,12 @@
</template>
<template #passRate="{ record }">
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN" class="mr-[8px] w-[100px]">
<div class="mr-[8px] w-[100px]">
<StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" />
</div>
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN" class="text-[var(--color-text-1)]">
<div class="text-[var(--color-text-1)]">
{{ `${defaultCountDetailMap[record.id]?.passRate ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
</div>
<span v-else> - </span>
</template>
<template #passRateTitleSlot="{ columnConfig }">
<div class="flex items-center text-[var(--color-text-3)]">
@ -206,13 +188,8 @@
</div>
</template>
<template #functionalCaseCount="{ record }">
<a-popover
v-if="record.type === testPlanTypeEnum.TEST_PLAN"
position="bottom"
content-class="p-[16px]"
:disabled="getFunctionalCount(record.id) < 1"
>
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN">{{ getFunctionalCount(record.id) }}</div>
<a-popover position="bottom" content-class="p-[16px]" :disabled="getFunctionalCount(record.id) < 1">
<div>{{ getFunctionalCount(record.id) }}</div>
<template #content>
<table class="min-w-[140px] max-w-[176px]">
<tr>
@ -250,7 +227,6 @@
</table>
</template>
</a-popover>
<span v-else>-</span>
</template>
<template #operation="{ record }">
@ -403,6 +379,7 @@
import BatchMoveOrCopy from './batchMoveOrCopy.vue';
import ScheduledModal from './scheduledModal.vue';
import StatusProgress from './statusProgress.vue';
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
import {
addTestPlan,
@ -628,16 +605,6 @@
}
}
//
function hasIndent(record: TestPlanItem) {
return (showType.value === 'ALL' || showType.value === 'GROUP') &&
record.type === testPlanTypeEnum.TEST_PLAN &&
record.groupId &&
record.groupId !== 'NONE'
? 'pl-[36px]'
: '';
}
const batchCopyActions = [
{
label: 'common.copy',
@ -742,6 +709,14 @@
},
];
const configReportActions: ActionsItem[] = [
{
label: 'testPlan.planConfigReport',
eventTag: 'configReport',
permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
},
];
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
function getFunctionalCount(id: string) {
return defaultCountDetailMap.value[id]?.caseTotal ?? 0;
@ -779,11 +754,15 @@
? []
: archiveActions;
const reportAction =
planStatus !== 'ARCHIVED' && record.type === testPlanTypeEnum.GROUP ? [...configReportActions] : [];
//
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
return [
...copyAction,
...scheduledTaskAction,
...reportAction,
{
label: 'common.delete',
danger: true,
@ -796,6 +775,7 @@
...copyAction,
...archiveAction,
...scheduledTaskAction,
...reportAction,
{
isDivider: true,
},
@ -1316,6 +1296,26 @@
activeRecord.value = cloneDeep(record);
showStatusDeleteModal.value = true;
}
// TODO
async function configReportHandler(record: TestPlanItem) {
try {
// await generateReport({
// projectId: appStore.currentProjectId,
// testPlanId: record.id,
// triggerMode: 'MANUAL',
// });
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_CONFIG,
query: {
id: record.id,
type: record.type === testPlanTypeEnum.GROUP ? 'GROUP' : 'TEST_PLAN',
},
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function handleDragChange(params: DragSortParams) {
@ -1401,6 +1401,9 @@
case 'delete':
deleteStatusHandler(record);
break;
case 'configReport':
configReportHandler(record);
break;
case 'archive':
archiveHandle(record);
break;
@ -1422,9 +1425,6 @@
}
}
}
function getIconClass(record: TestPlanItem) {
return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
}
/** *
* 高级检索

View File

@ -24,6 +24,15 @@
</div>
</div>
<div class="markdown-body" style="margin-left: 48px" v-html="item.contentText"></div>
<div v-if="props.showStepResult" class="ml-[48px] mt-[8px]">
<StepDetail
:step-list="getStepData(item.stepsExecResult)"
is-disabled
is-preview
is-test-plan
:is-disabled-test-plan="false"
/>
</div>
<div class="ml-[48px] mt-[8px] flex text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
<div>
@ -42,37 +51,37 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import dayjs from 'dayjs';
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue';
import StepDetail from '@/views/case-management/caseManagementFeature/components/addStep.vue';
import { executeHistory } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { characterLimit } from '@/utils';
import type { ExecuteHistoryItem } from '@/models/testPlan/testPlan';
import type { ExecuteHistoryItem, ExecuteHistoryType } from '@/models/testPlan/testPlan';
const { t } = useI18n();
const props = defineProps<{
caseId: string;
testPlanCaseId: string;
loadListFun: (params: ExecuteHistoryType) => Promise<ExecuteHistoryItem[]>;
extraParams: ExecuteHistoryType;
showStepResult?: boolean; //
}>();
const executeHistoryList = ref<ExecuteHistoryItem[]>([]);
const route = useRoute();
const loading = ref<boolean>(false);
async function initList() {
loading.value = true;
try {
executeHistoryList.value = await executeHistory({
caseId: props.caseId,
id: props.testPlanCaseId,
testPlanId: route.query.id as string,
if (props.loadListFun) {
executeHistoryList.value = await props.loadListFun({
...props.extraParams,
});
}
} catch (error) {
console.log(error);
} finally {
@ -80,16 +89,30 @@
}
}
onBeforeMount(() => {
initList();
function getStepData(steps: string) {
if (steps) {
return JSON.parse(steps).map((item: any) => {
return {
id: item.id,
step: item.desc,
expected: item.result,
actualResult: item.actualResult,
executeResult: item.executeResult,
};
});
}
return [];
}
watch(
() => props.caseId,
() => props.extraParams.caseId,
(val) => {
if (val) {
initList();
}
},
{
immediate: true,
}
);
</script>

View File

@ -193,8 +193,12 @@
/>
<ExecutionHistory
v-if="activeTab === 'executionHistory'"
:case-id="activeCaseId"
:test-plan-case-id="activeId"
:extra-params="{
caseId:activeCaseId,
id: activeId,
testPlanId: route.query.id as string,
}"
:load-list-fun="executeHistory"
/>
</div>
</a-spin>
@ -245,6 +249,7 @@
import { getBugList } from '@/api/modules/bug-management';
import {
associateBugToPlan,
executeHistory,
getCaseDetail,
getPlanDetailFeatureCaseList,
getTestPlanDetail,

View File

@ -33,15 +33,17 @@
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }}
</MsButton>
<!-- TODO 等待联调 接口需要调整和增加 -->
<MsTableMoreAction :list="reportMoreAction" @select="handleMoreReportSelect">
<MsButton
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && detail.status !== 'ARCHIVED'"
type="button"
status="default"
@click="handleGenerateReport"
>
<MsIcon type="icon-icon_generate_report" class="mr-[8px]" />
{{ t('testPlan.testPlanDetail.generateReport') }}
</MsButton>
</MsTableMoreAction>
<MsButton
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && detail.status !== 'ARCHIVED'"
type="button"
@ -185,6 +187,7 @@
import { ModuleTreeNode } from '@/models/common';
import type { PassRateCountDetail, TestPlanDetail, TestPlanItem } from '@/models/testPlan/testPlan';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const userStore = useUserStore();
const appStore = useAppStore();
@ -380,7 +383,7 @@
const showPlanDrawer = ref(false);
//
// TODO
async function handleGenerateReport() {
try {
loading.value = true;
@ -397,6 +400,52 @@
loading.value = false;
}
}
// TODO
function configReportHandler() {
try {
// await generateReport({
// projectId: appStore.currentProjectId,
// testPlanId: record.id,
// triggerMode: 'MANUAL',
// });
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_CONFIG,
query: {
id: detail.value.id,
type: detail.value.type === testPlanTypeEnum.GROUP ? 'GROUP' : 'TEST_PLAN',
},
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const reportMoreAction: ActionsItem[] = [
{
label: t('testPlan.planAutomaticGeneration'),
eventTag: 'autoGeneration',
permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
},
{
label: t('testPlan.planConfigReport'),
eventTag: 'configReport',
permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
},
];
function handleMoreReportSelect(item: ActionsItem) {
switch (item.eventTag) {
case 'autoGeneration':
handleGenerateReport();
break;
case 'configReport':
configReportHandler();
break;
default:
break;
}
}
// |
const isCopy = ref<boolean>(false);

View File

@ -12,7 +12,7 @@
<template #tbutton>
<PlanDetailHeaderRight share-id="" :detail="detail" />
</template>
<PlanDetail is-drawer :detail-info="detail" @update-success="getDetail()" />
<ViewReport v-model:card-list="cardItemList" :detail-info="detail" :is-group="false" is-preview is-drawer />
</MsDrawer>
</template>
@ -22,13 +22,15 @@
import { cloneDeep } from 'lodash-es';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
import PlanDetailHeaderRight from '@/views/test-plan/report/detail/component/planDetailHeaderRight.vue';
import PlanDetailHeaderRight from '@/views/test-plan/report/detail/component/system-card/planDetailHeaderRight.vue';
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
import { getReportDetail } from '@/api/modules/test-plan/report';
import { defaultReportDetail } from '@/config/testPlan';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { defaultSingleConfig } from '@/views/test-plan/report/detail/component/reportConfig';
const props = defineProps<{
reportId: string;
@ -40,6 +42,8 @@
const route = useRoute();
const cardItemList = ref<configItem[]>(cloneDeep(defaultSingleConfig));
const shareId = ref<string>(route.query.shareId as string);
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));

View File

@ -144,5 +144,7 @@ export default {
'testPlan.plan': 'Test plan',
'testPlan.planTip':
'1. Create a test set for business classification testing; 2. Select the test set associated use case',
'testPlan.planStartToEndTimeTip': '测试计划已超时',
'testPlan.planStartToEndTimeTip': 'The test plan timed out',
'testPlan.planConfigReport': 'Configuration Report',
'testPlan.planAutomaticGeneration': 'Automatic generation',
};

View File

@ -135,4 +135,6 @@ export default {
'testPlan.plan': '测试规划',
'testPlan.planTip': '1.创建测试点进行业务分类测试2.选择测试点关联用例',
'testPlan.planStartToEndTimeTip': '测试计划已超时',
'testPlan.planConfigReport': '自定义报告',
'testPlan.planAutomaticGeneration': '自动生成',
};