feat(测试计划): 测试计划报告自定义配置以报告及预览页面初稿待联调&计划组列表调整
This commit is contained in:
parent
3e49eab864
commit
e8ffc7b3b3
|
@ -100,6 +100,7 @@
|
||||||
placeholder: 'editor.placeholder',
|
placeholder: 'editor.placeholder',
|
||||||
draggable: false,
|
draggable: false,
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
|
editable: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -137,6 +138,14 @@
|
||||||
if (props.raw !== editor.value?.getHTML()) {
|
if (props.raw !== editor.value?.getHTML()) {
|
||||||
editor.value?.commands.setContent(props.raw);
|
editor.value?.commands.setContent(props.raw);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.editable,
|
||||||
|
(val) => {
|
||||||
|
// 更新富文本的可编辑配置
|
||||||
|
editor.value?.setOptions({ editable: val });
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
@ -347,7 +356,7 @@
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
editable: !props.editable,
|
editable: props.editable,
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
debounceOnUpdate();
|
debounceOnUpdate();
|
||||||
},
|
},
|
||||||
|
|
|
@ -62,6 +62,7 @@ export enum TestPlanRouteEnum {
|
||||||
TEST_PLAN = 'testPlan',
|
TEST_PLAN = 'testPlan',
|
||||||
TEST_PLAN_INDEX = 'testPlanIndex',
|
TEST_PLAN_INDEX = 'testPlanIndex',
|
||||||
TEST_PLAN_INDEX_DETAIL = 'testPlanIndexDetail',
|
TEST_PLAN_INDEX_DETAIL = 'testPlanIndexDetail',
|
||||||
|
TEST_PLAN_INDEX_CONFIG = 'testPlanIndexConfig',
|
||||||
TEST_PLAN_INDEX_DETAIL_FEATURE_CASE_DETAIL = 'testPlanIndexDetailFeatureCaseDetail',
|
TEST_PLAN_INDEX_DETAIL_FEATURE_CASE_DETAIL = 'testPlanIndexDetailFeatureCaseDetail',
|
||||||
TEST_PLAN_REPORT = 'testPlanReport',
|
TEST_PLAN_REPORT = 'testPlanReport',
|
||||||
TEST_PLAN_REPORT_DETAIL = 'testPlanReportDetail',
|
TEST_PLAN_REPORT_DETAIL = 'testPlanReportDetail',
|
||||||
|
|
|
@ -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 {};
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
// 实时
|
// 实时
|
||||||
|
|
||||||
|
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
|
||||||
|
|
||||||
export interface RealTaskCenterApiCaseItem {
|
export interface RealTaskCenterApiCaseItem {
|
||||||
organizationName: string; // 所属组织
|
organizationName: string; // 所属组织
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
@ -14,6 +17,13 @@ export interface RealTaskCenterApiCaseItem {
|
||||||
operationTime: string;
|
operationTime: string;
|
||||||
integrated: boolean; // 是否为集合报告
|
integrated: boolean; // 是否为集合报告
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestPlanTaskCenterItem extends RealTaskCenterApiCaseItem {
|
||||||
|
children: TestPlanTaskCenterItem[];
|
||||||
|
childrenCount: number;
|
||||||
|
groupId: string;
|
||||||
|
type: keyof typeof testPlanTypeEnum;
|
||||||
|
}
|
||||||
// 定时任务
|
// 定时任务
|
||||||
export interface TimingTaskCenterApiCaseItem {
|
export interface TimingTaskCenterApiCaseItem {
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
|
|
|
@ -34,4 +34,5 @@ export interface ApiOrScenarioCaseItem {
|
||||||
executeUser: string;
|
executeUser: string;
|
||||||
bugCount: number;
|
bugCount: number;
|
||||||
reportId: string;
|
reportId: string;
|
||||||
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
|
||||||
|
|
||||||
export interface countDetail {
|
export interface countDetail {
|
||||||
success: number;
|
success: number;
|
||||||
error: number;
|
error: number;
|
||||||
|
@ -28,8 +30,11 @@ export interface PlanReportDetail {
|
||||||
apiBugCount: number; // 接口用例明细bug总数
|
apiBugCount: number; // 接口用例明细bug总数
|
||||||
scenarioBugCount: number; // 场景用例明细bug总数
|
scenarioBugCount: number; // 场景用例明细bug总数
|
||||||
testPlanName: string;
|
testPlanName: string;
|
||||||
|
resultStatus?: string; // 报告结果
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type detailCountKey = 'functionalCount' | 'apiCaseCount' | 'apiScenarioCount';
|
||||||
|
|
||||||
export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO';
|
export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO';
|
||||||
|
|
||||||
export interface ReportMetricsItemModel {
|
export interface ReportMetricsItemModel {
|
||||||
|
@ -49,3 +54,18 @@ export interface StatusListType {
|
||||||
rateKey: string;
|
rateKey: string;
|
||||||
key: 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[];
|
||||||
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ const TestPlan: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'testPlanReportDetail',
|
path: 'testPlanReportDetail',
|
||||||
name: TestPlanRouteEnum.TEST_PLAN_REPORT_DETAIL,
|
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: {
|
meta: {
|
||||||
locale: 'menu.apiTest.reportDetail',
|
locale: 'menu.apiTest.reportDetail',
|
||||||
roles: ['PROJECT_TEST_PLAN_REPORT:READ'],
|
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',
|
path: 'testPlanIndexDetailFeatureCaseDetail',
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
import ReportDetailHeader from './reportDetailHeader.vue';
|
import ReportDetailHeader from './reportDetailHeader.vue';
|
||||||
import reportInfoHeader from './step/reportInfoHeaders.vue';
|
import reportInfoHeader from './step/reportInfoHeaders.vue';
|
||||||
import TiledList from './tiledList.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 { toolTipConfig } from '@/config/testPlan';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
|
@ -33,7 +33,9 @@
|
||||||
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && !props.isDisabledTestPlan"
|
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && !props.isDisabledTestPlan"
|
||||||
#actualResult="{ record }"
|
#actualResult="{ record }"
|
||||||
>
|
>
|
||||||
|
<div v-if="props.isPreview">{{ record.actualResult }}</div>
|
||||||
<a-textarea
|
<a-textarea
|
||||||
|
v-else
|
||||||
v-model="record.actualResult"
|
v-model="record.actualResult"
|
||||||
:max-length="1000"
|
:max-length="1000"
|
||||||
size="mini"
|
size="mini"
|
||||||
|
@ -44,7 +46,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #lastExecResult="{ record }">
|
<template #lastExecResult="{ record }">
|
||||||
<a-select
|
<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"
|
v-model:model-value="record.executeResult"
|
||||||
:placeholder="t('common.pleaseSelect')"
|
:placeholder="t('common.pleaseSelect')"
|
||||||
class="param-input w-full"
|
class="param-input w-full"
|
||||||
|
@ -105,6 +107,7 @@
|
||||||
isScrollY?: boolean;
|
isScrollY?: boolean;
|
||||||
isTestPlan?: boolean;
|
isTestPlan?: boolean;
|
||||||
isDisabledTestPlan?: boolean;
|
isDisabledTestPlan?: boolean;
|
||||||
|
isPreview?: boolean; // 仅预览不展示状态可操作下拉和文本框
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
|
|
@ -19,17 +19,21 @@
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
:action-config="tableBatchActions"
|
:action-config="tableBatchActions"
|
||||||
:selectable="hasOperationPermission"
|
:selectable="hasOperationPermission"
|
||||||
|
:expanded-keys="expandedKeys"
|
||||||
v-on="propsEvent"
|
v-on="propsEvent"
|
||||||
@batch-action="handleTableBatch"
|
@batch-action="handleTableBatch"
|
||||||
>
|
>
|
||||||
|
<!-- TOTO 等待联调 后台接口需要调整 -->
|
||||||
<template #resourceNum="{ record }">
|
<template #resourceNum="{ record }">
|
||||||
<div
|
<div class="flex items-center">
|
||||||
v-if="!record.integrated"
|
<PlanExpandRow
|
||||||
type="text"
|
v-model:expanded-keys="expandedKeys"
|
||||||
class="one-line-text w-full"
|
num-key="resourceNum"
|
||||||
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
|
:record="record"
|
||||||
@click="showDetail(record)"
|
:permission="permissionsMap[props.group].jump"
|
||||||
>{{ record.resourceNum }}
|
@action="showDetail(record)"
|
||||||
|
@expand="expandHandler(record)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #resourceName="{ record }">
|
<template #resourceName="{ record }">
|
||||||
|
@ -114,6 +118,7 @@
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
import ExecStatus from '@/views/test-plan/report/component/execStatus.vue';
|
import ExecStatus from '@/views/test-plan/report/component/execStatus.vue';
|
||||||
import ExecutionStatus from '@/views/test-plan/report/component/reportStatus.vue';
|
import ExecutionStatus from '@/views/test-plan/report/component/reportStatus.vue';
|
||||||
|
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
batchStopRealOrgPlan,
|
batchStopRealOrgPlan,
|
||||||
|
@ -134,6 +139,7 @@
|
||||||
import { hasAnyPermission } from '@/utils/permission';
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
import { BatchApiParams } from '@/models/common';
|
import { BatchApiParams } from '@/models/common';
|
||||||
|
import type { TestPlanTaskCenterItem } from '@/models/projectManagement/taskCenter';
|
||||||
import { ReportExecStatus } from '@/enums/apiEnum';
|
import { ReportExecStatus } from '@/enums/apiEnum';
|
||||||
import { PlanReportStatus } from '@/enums/reportEnum';
|
import { PlanReportStatus } from '@/enums/reportEnum';
|
||||||
import { RouteEnum } from '@/enums/routeEnum';
|
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() {
|
function searchList() {
|
||||||
resetSelector();
|
resetSelector();
|
||||||
initData();
|
initData();
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(() => {
|
||||||
initData();
|
initData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -487,4 +503,11 @@
|
||||||
await tableStore.initColumn(tableKeysMap[props.group], groupColumnsMap[props.group], 'drawer', true);
|
await tableStore.initColumn(tableKeysMap[props.group], groupColumnsMap[props.group], 'drawer', true);
|
||||||
</script>
|
</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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
|
||||||
}
|
|
||||||
// 有5个的时候,上面2个,下面3个
|
|
||||||
&[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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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 {};
|
|
@ -26,9 +26,13 @@
|
||||||
v-model:visible="reportVisible"
|
v-model:visible="reportVisible"
|
||||||
:report-id="apiReportId"
|
:report-id="apiReportId"
|
||||||
do-not-show-share
|
do-not-show-share
|
||||||
:is-scenario="props.activeTab === 'scenarioCase'"
|
:is-scenario="props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL"
|
||||||
:report-detail="props.activeTab === 'scenarioCase' ? reportScenarioDetail : reportCaseDetail"
|
:report-detail="
|
||||||
:get-report-step-detail="props.activeTab === 'scenarioCase' ? reportStepDetail : reportCaseStepDetail"
|
props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL ? reportScenarioDetail : reportCaseDetail
|
||||||
|
"
|
||||||
|
:get-report-step-detail="
|
||||||
|
props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL ? reportStepDetail : reportCaseStepDetail
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -56,15 +60,18 @@
|
||||||
import { ReportEnum } from '@/enums/reportEnum';
|
import { ReportEnum } from '@/enums/reportEnum';
|
||||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||||
|
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
|
||||||
|
|
||||||
import { casePriorityOptions, lastReportStatusListOptions } from '@/views/api-test/components/config';
|
import { casePriorityOptions, lastReportStatusListOptions } from '@/views/api-test/components/config';
|
||||||
|
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
|
||||||
|
|
||||||
const { openNewPage } = useOpenNewPage();
|
const { openNewPage } = useOpenNewPage();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reportId: string;
|
reportId: string;
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
activeTab: string;
|
activeType: ReportCardTypeEnum;
|
||||||
|
isPreview?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const columns: MsTableColumn = [
|
const columns: MsTableColumn = [
|
||||||
|
@ -128,13 +135,6 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getReportApiList = () => {
|
|
||||||
if (props.activeTab === 'apiCase') {
|
|
||||||
return getApiPage;
|
|
||||||
}
|
|
||||||
return getScenarioPage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useApiTable = useTable(getApiPage, {
|
const useApiTable = useTable(getApiPage, {
|
||||||
scroll: { x: '100%' },
|
scroll: { x: '100%' },
|
||||||
columns,
|
columns,
|
||||||
|
@ -149,7 +149,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentCaseTable = computed(() => {
|
const currentCaseTable = computed(() => {
|
||||||
return props.activeTab === 'apiCase' ? useApiTable : useScenarioTable;
|
return props.activeType === ReportCardTypeEnum.API_CASE_DETAIL ? useApiTable : useScenarioTable;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadCaseList() {
|
async function loadCaseList() {
|
||||||
|
@ -170,7 +170,7 @@
|
||||||
|
|
||||||
// 去接口用例详情页面
|
// 去接口用例详情页面
|
||||||
function toDetail(record: PlanDetailApiCaseItem) {
|
function toDetail(record: PlanDetailApiCaseItem) {
|
||||||
if (props.activeTab === 'scenarioCase') {
|
if (props.activeType === ReportCardTypeEnum.SCENARIO_CASE_DETAIL) {
|
||||||
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
|
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
pId: record.projectId,
|
pId: record.projectId,
|
||||||
|
@ -183,16 +183,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watchEffect(() => {
|
||||||
() => props.activeTab,
|
if (props.reportId && props.activeType && props.isPreview) {
|
||||||
() => {
|
|
||||||
currentCaseTable.value.resetFilterParams();
|
currentCaseTable.value.resetFilterParams();
|
||||||
currentCaseTable.value.resetPagination();
|
currentCaseTable.value.resetPagination();
|
||||||
loadCaseList();
|
loadCaseList();
|
||||||
|
} else {
|
||||||
|
currentCaseTable.value.propsRes.value.data = detailTableExample[props.activeType];
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadCaseList();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -3,17 +3,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeMount } from 'vue';
|
|
||||||
|
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
|
||||||
import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
|
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<{
|
const props = defineProps<{
|
||||||
reportId: string;
|
reportId: string;
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
|
isPreview?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const columns: MsTableColumn = [
|
const columns: MsTableColumn = [
|
||||||
|
@ -73,8 +79,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.reportId) {
|
if (props.reportId && props.isPreview) {
|
||||||
loadCaseList();
|
loadCaseList();
|
||||||
|
} else {
|
||||||
|
propsRes.value.data = detailTableExample[ReportCardTypeEnum.BUG_DETAIL];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -8,31 +8,60 @@
|
||||||
</template>
|
</template>
|
||||||
<template #lastExecResult="{ record }">
|
<template #lastExecResult="{ record }">
|
||||||
<ExecuteResult :execute-result="record.executeResult" />
|
<ExecuteResult :execute-result="record.executeResult" />
|
||||||
|
<MsButton class="ml-[8px]" :disabled="!props.isPreview" @click="openExecuteHistory(record)">{{
|
||||||
|
t('common.detail')
|
||||||
|
}}</MsButton>
|
||||||
</template>
|
</template>
|
||||||
</MsBaseTable>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeMount } from 'vue';
|
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 MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||||
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.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 { 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 { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||||
|
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
|
||||||
|
|
||||||
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
|
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
|
||||||
|
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reportId: string;
|
reportId: string;
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
activeTab: string;
|
isPreview?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
const { t } = useI18n();
|
||||||
const columns: MsTableColumn = [
|
const columns: MsTableColumn = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
|
@ -58,6 +87,18 @@
|
||||||
},
|
},
|
||||||
width: 180,
|
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',
|
title: 'common.belongModule',
|
||||||
dataIndex: 'moduleName',
|
dataIndex: 'moduleName',
|
||||||
|
@ -71,18 +112,7 @@
|
||||||
slotName: 'caseLevel',
|
slotName: 'caseLevel',
|
||||||
width: 120,
|
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',
|
title: 'testPlan.featureCase.executor',
|
||||||
dataIndex: 'executeUser',
|
dataIndex: 'executeUser',
|
||||||
|
@ -111,8 +141,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.reportId) {
|
if (props.reportId && props.isPreview) {
|
||||||
loadCaseList();
|
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>
|
</script>
|
|
@ -23,7 +23,7 @@
|
||||||
<ExecutionStatus :status="filterContent.value" />
|
<ExecutionStatus :status="filterContent.value" />
|
||||||
</template>
|
</template>
|
||||||
<template #operation="{ record }">
|
<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')
|
t('report.detail.testPlanGroup.viewReport')
|
||||||
}}</MsButton>
|
}}</MsButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -49,6 +49,9 @@
|
||||||
import { PlanReportStatus } from '@/enums/reportEnum';
|
import { PlanReportStatus } from '@/enums/reportEnum';
|
||||||
import { RouteEnum } from '@/enums/routeEnum';
|
import { RouteEnum } from '@/enums/routeEnum';
|
||||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||||
|
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
|
||||||
|
|
||||||
|
import { detailTableExample } from '@/views/test-plan/report/detail/component/reportConfig';
|
||||||
|
|
||||||
const { openNewPage } = useOpenNewPage();
|
const { openNewPage } = useOpenNewPage();
|
||||||
|
|
||||||
|
@ -57,9 +60,13 @@
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reportId: string;
|
reportId: string;
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
currentMode: string;
|
isPreview?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const innerCurrentMode = defineModel<string>('currentMode', {
|
||||||
|
default: 'drawer',
|
||||||
|
});
|
||||||
|
|
||||||
const statusResultOptions = computed(() => {
|
const statusResultOptions = computed(() => {
|
||||||
return Object.keys(PlanReportStatus).map((key) => {
|
return Object.keys(PlanReportStatus).map((key) => {
|
||||||
return {
|
return {
|
||||||
|
@ -132,8 +139,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.reportId) {
|
if (props.reportId && props.isPreview) {
|
||||||
loadReportDetailList();
|
loadReportDetailList();
|
||||||
|
} else {
|
||||||
|
propsRes.value.data = detailTableExample[ReportCardTypeEnum.SUB_PLAN_DETAIL];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -143,7 +152,7 @@
|
||||||
|
|
||||||
function openReport(record: PlanReportDetail) {
|
function openReport(record: PlanReportDetail) {
|
||||||
independentReportId.value = record.id;
|
independentReportId.value = record.id;
|
||||||
if (props.currentMode === 'drawer') {
|
if (innerCurrentMode.value === 'drawer') {
|
||||||
reportVisible.value = true;
|
reportVisible.value = true;
|
||||||
} else {
|
} else {
|
||||||
openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, {
|
openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, {
|
|
@ -17,7 +17,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import MsCard from '@/components/pure/ms-card/index.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';
|
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<MsCard class="mb-[16px]" simple auto-height auto-width>
|
<div :class="`${hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId ? '' : 'cursor-not-allowed'}`">
|
||||||
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
|
|
||||||
<div
|
|
||||||
:class="`${hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId ? '' : 'cursor-not-allowed'}`"
|
|
||||||
>
|
|
||||||
<MsRichText
|
<MsRichText
|
||||||
v-model:raw="innerSummary.summary"
|
v-model:raw="innerSummary.summary"
|
||||||
v-model:filedIds="innerSummary.richTextTmpFileIds"
|
v-model:filedIds="innerSummary.richTextTmpFileIds"
|
||||||
:upload-image="handleUploadImage"
|
:upload-image="handleUploadImage"
|
||||||
:preview-url="ReportPlanPreviewImageUrl"
|
:preview-url="ReportPlanPreviewImageUrl"
|
||||||
class="mt-[8px] w-full"
|
class="mt-[8px] w-full"
|
||||||
:editable="!!shareId"
|
:editable="props.canEdit"
|
||||||
|
@click="handleClick"
|
||||||
/>
|
/>
|
||||||
<MsFormItemSub
|
<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')"
|
:text="t('report.detail.oneClickSummary')"
|
||||||
:show-fill-icon="true"
|
:show-fill-icon="true"
|
||||||
@fill="handleSummary"
|
@fill="handleSummary"
|
||||||
|
@ -21,25 +18,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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]"
|
class="mt-[16px] flex items-center gap-[12px]"
|
||||||
>
|
>
|
||||||
<a-button type="primary" @click="handleUpdateReportDetail">{{ t('common.save') }}</a-button>
|
<a-button type="primary" @click="handleUpdateReportDetail">{{ t('common.save') }}</a-button>
|
||||||
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
|
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
|
||||||
</div>
|
</div>
|
||||||
</MsCard>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useVModel } from '@vueuse/core';
|
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 MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
||||||
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
|
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
|
||||||
|
|
||||||
import { editorUploadFile } from '@/api/modules/test-plan/report';
|
import { editorUploadFile } from '@/api/modules/test-plan/report';
|
||||||
import { ReportPlanPreviewImageUrl } from '@/api/requrls/test-plan/report';
|
import { ReportPlanPreviewImageUrl } from '@/api/requrls/test-plan/report';
|
||||||
|
import useDoubleClick from '@/hooks/useDoubleClick';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { hasAnyPermission } from '@/utils/permission';
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
|
@ -54,11 +50,13 @@
|
||||||
showButton: boolean;
|
showButton: boolean;
|
||||||
isPlanGroup: boolean;
|
isPlanGroup: boolean;
|
||||||
detail: PlanReportDetail;
|
detail: PlanReportDetail;
|
||||||
|
canEdit: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'updateSummary'): void;
|
(e: 'updateSummary'): void;
|
||||||
(e: 'cancel'): void;
|
(e: 'cancel'): void;
|
||||||
|
(e: 'dblclick'): void;
|
||||||
(e: 'handleSummary', content: string): void;
|
(e: 'handleSummary', content: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -128,6 +126,11 @@
|
||||||
function handleSummary() {
|
function handleSummary() {
|
||||||
emit('handleSummary', summaryContent.value);
|
emit('handleSummary', summaryContent.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitDoubleClick() {
|
||||||
|
emit('dblclick');
|
||||||
|
}
|
||||||
|
const { handleClick } = useDoubleClick(emitDoubleClick);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
// 有5个的时候,上面2个,下面3个
|
||||||
|
&[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>
|
|
@ -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>
|
|
@ -1,20 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<PlanGroupDetail v-if="isGroup" :detail-info="detail" @update-success="getDetail()" />
|
<ViewReport v-model:card-list="cardItemList" :detail-info="detail" :is-group="isGroup" is-preview />
|
||||||
<PlanDetail v-else :detail-info="detail" @update-success="getDetail()" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// TODO 待联调 需要接口更新后联调下
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
|
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
|
||||||
import PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
|
|
||||||
|
|
||||||
import { getReportDetail } from '@/api/modules/test-plan/report';
|
import { getReportDetail } from '@/api/modules/test-plan/report';
|
||||||
import { defaultReportDetail } from '@/config/testPlan';
|
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 route = useRoute();
|
||||||
const reportId = ref<string>(route.query.id as string);
|
const reportId = ref<string>(route.query.id as string);
|
||||||
|
@ -23,7 +24,10 @@
|
||||||
|
|
||||||
const isGroup = computed(() => route.query.type === 'GROUP');
|
const isGroup = computed(() => route.query.type === 'GROUP');
|
||||||
|
|
||||||
|
const cardItemList = ref<configItem[]>([]);
|
||||||
|
|
||||||
async function getDetail() {
|
async function getDetail() {
|
||||||
|
cardItemList.value = isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
|
||||||
try {
|
try {
|
||||||
detail.value = await getReportDetail(reportId.value);
|
detail.value = await getReportDetail(reportId.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
|
@ -1,21 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<PlanGroupDetail v-if="isGroup" :detail-info="detail" />
|
<ViewReport v-model:card-list="cardItemList" :detail-info="detail" :is-group="isGroup" is-preview />
|
||||||
<PlanDetail v-else :detail-info="detail" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// TODO 待联调 分享页面也要调整
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
|
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
|
||||||
import PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
|
|
||||||
|
|
||||||
import { getReportDetail, planGetShareHref } from '@/api/modules/test-plan/report';
|
import { getReportDetail, planGetShareHref } from '@/api/modules/test-plan/report';
|
||||||
import { defaultReportDetail } from '@/config/testPlan';
|
import { defaultReportDetail } from '@/config/testPlan';
|
||||||
import { NOT_FOUND_RESOURCE } from '@/router/constants';
|
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 route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -23,7 +24,9 @@
|
||||||
const isGroup = computed(() => route.query.type === 'GROUP');
|
const isGroup = computed(() => route.query.type === 'GROUP');
|
||||||
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
||||||
|
|
||||||
|
const cardItemList = ref<configItem[]>([]);
|
||||||
async function getShareDetail() {
|
async function getShareDetail() {
|
||||||
|
cardItemList.value = isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
|
||||||
try {
|
try {
|
||||||
const hrefShareDetail = await planGetShareHref(route.query.shareId as string);
|
const hrefShareDetail = await planGetShareHref(route.query.shareId as string);
|
||||||
reportId.value = hrefShareDetail.reportId;
|
reportId.value = hrefShareDetail.reportId;
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
'report.passRateTip': 'Pass rate: successful cases in the plan / cases in the plan * 100%',
|
'report.passRateTip': 'Pass rate: successful cases in the plan / cases in the plan * 100%',
|
||||||
'report.detail.reportSummary': 'Report summary',
|
'report.detail.reportSummary': 'Report summary',
|
||||||
'report.detail.bugDetails': 'Bug details',
|
'report.detail.bugDetails': 'Bug details',
|
||||||
'report.detail.featureCaseDetails': 'Feature case details',
|
'report.detail.featureCaseDetails': 'Case details',
|
||||||
'report.detail.executionAnalysis': 'Execution Analysis',
|
'report.detail.executionAnalysis': 'Execution Analysis',
|
||||||
'report.detail.threshold': 'Pass threshold',
|
'report.detail.threshold': 'Pass threshold',
|
||||||
'report.detail.performCompletion': 'Perform completion',
|
'report.detail.performCompletion': 'Perform completion',
|
||||||
|
@ -36,8 +36,8 @@ export default {
|
||||||
'report.detail.scenarioUseCaseAnalysis': 'Scenario use case analysis',
|
'report.detail.scenarioUseCaseAnalysis': 'Scenario use case analysis',
|
||||||
'report.detail.number': 'number',
|
'report.detail.number': 'number',
|
||||||
'report.detail.level': 'level',
|
'report.detail.level': 'level',
|
||||||
'report.detail.apiCaseDetails': 'Api use case details',
|
'report.detail.apiCaseDetails': 'Api details',
|
||||||
'report.detail.scenarioCaseDetails': 'Scenario use case details',
|
'report.detail.scenarioCaseDetails': 'Scenario details',
|
||||||
'report.detail.oneClickSummary': 'One click report summary',
|
'report.detail.oneClickSummary': 'One click report summary',
|
||||||
'report.detail.testPlanTotal': 'Total plan',
|
'report.detail.testPlanTotal': 'Total plan',
|
||||||
'report.detail.testPlanCaseTotal': 'Total use cases',
|
'report.detail.testPlanCaseTotal': 'Total use cases',
|
||||||
|
@ -46,4 +46,13 @@ export default {
|
||||||
'report.detail.testPlanGroup.viewReport': 'View Report',
|
'report.detail.testPlanGroup.viewReport': 'View Report',
|
||||||
'report.detail.testReport': 'Planning report',
|
'report.detail.testReport': 'Planning report',
|
||||||
'report.detail.testPlanGroupReport': 'Planning groups 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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
'report.completed': '已完成',
|
'report.completed': '已完成',
|
||||||
'report.detail.reportSummary': '报告总结',
|
'report.detail.reportSummary': '报告总结',
|
||||||
'report.detail.bugDetails': '缺陷明细',
|
'report.detail.bugDetails': '缺陷明细',
|
||||||
'report.detail.featureCaseDetails': '功能用例明细',
|
'report.detail.featureCaseDetails': '用例明细',
|
||||||
'report.detail.executionAnalysis': '执行分析',
|
'report.detail.executionAnalysis': '执行分析',
|
||||||
'report.detail.threshold': '通过阈值',
|
'report.detail.threshold': '通过阈值',
|
||||||
'report.detail.performCompletion': '执行完成率',
|
'report.detail.performCompletion': '执行完成率',
|
||||||
|
@ -36,8 +36,8 @@ export default {
|
||||||
'report.detail.scenarioUseCaseAnalysis': '场景用例分析',
|
'report.detail.scenarioUseCaseAnalysis': '场景用例分析',
|
||||||
'report.detail.number': '个',
|
'report.detail.number': '个',
|
||||||
'report.detail.level': '等级',
|
'report.detail.level': '等级',
|
||||||
'report.detail.apiCaseDetails': '接口用例明细',
|
'report.detail.apiCaseDetails': '接口明细',
|
||||||
'report.detail.scenarioCaseDetails': '场景用例明细',
|
'report.detail.scenarioCaseDetails': '场景明细',
|
||||||
'report.detail.oneClickSummary': '一键填写报告总结',
|
'report.detail.oneClickSummary': '一键填写报告总结',
|
||||||
'report.detail.testPlanTotal': '计划总数',
|
'report.detail.testPlanTotal': '计划总数',
|
||||||
'report.detail.testPlanCaseTotal': '用例总数',
|
'report.detail.testPlanCaseTotal': '用例总数',
|
||||||
|
@ -46,4 +46,13 @@ export default {
|
||||||
'report.detail.testPlanGroup.viewReport': '查看报告',
|
'report.detail.testPlanGroup.viewReport': '查看报告',
|
||||||
'report.detail.testReport': '计划报告',
|
'report.detail.testReport': '计划报告',
|
||||||
'report.detail.testPlanGroupReport': '计划组报告',
|
'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': '系统内置,不可编辑',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
|
@ -97,29 +97,12 @@
|
||||||
</template> -->
|
</template> -->
|
||||||
<template #num="{ record }">
|
<template #num="{ record }">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<PlanExpandRow
|
||||||
v-if="record.type === testPlanTypeEnum.GROUP"
|
v-model:expanded-keys="expandedKeys"
|
||||||
class="mr-2 flex items-center"
|
:record="record"
|
||||||
@click="expandHandler(record)"
|
@action="openDetail(record.id)"
|
||||||
>
|
@expand="expandHandler(record)"
|
||||||
<MsIcon
|
|
||||||
type="icon-icon_split_turn-down_arrow"
|
|
||||||
class="arrowIcon mr-1 cursor-pointer text-[16px]"
|
|
||||||
:class="getIconClass(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">
|
<a-tooltip position="right" :disabled="!getSchedule(record.id)" :mouse-enter-delay="300">
|
||||||
<MsTag
|
<MsTag
|
||||||
v-if="getSchedule(record.id)"
|
v-if="getSchedule(record.id)"
|
||||||
|
@ -186,13 +169,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #passRate="{ record }">
|
<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" />
|
<StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" />
|
||||||
</div>
|
</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 : '-'}%` }}
|
{{ `${defaultCountDetailMap[record.id]?.passRate ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
|
||||||
</div>
|
</div>
|
||||||
<span v-else> - </span>
|
|
||||||
</template>
|
</template>
|
||||||
<template #passRateTitleSlot="{ columnConfig }">
|
<template #passRateTitleSlot="{ columnConfig }">
|
||||||
<div class="flex items-center text-[var(--color-text-3)]">
|
<div class="flex items-center text-[var(--color-text-3)]">
|
||||||
|
@ -206,13 +188,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #functionalCaseCount="{ record }">
|
<template #functionalCaseCount="{ record }">
|
||||||
<a-popover
|
<a-popover position="bottom" content-class="p-[16px]" :disabled="getFunctionalCount(record.id) < 1">
|
||||||
v-if="record.type === testPlanTypeEnum.TEST_PLAN"
|
<div>{{ getFunctionalCount(record.id) }}</div>
|
||||||
position="bottom"
|
|
||||||
content-class="p-[16px]"
|
|
||||||
:disabled="getFunctionalCount(record.id) < 1"
|
|
||||||
>
|
|
||||||
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN">{{ getFunctionalCount(record.id) }}</div>
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<table class="min-w-[140px] max-w-[176px]">
|
<table class="min-w-[140px] max-w-[176px]">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -250,7 +227,6 @@
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #operation="{ record }">
|
<template #operation="{ record }">
|
||||||
|
@ -403,6 +379,7 @@
|
||||||
import BatchMoveOrCopy from './batchMoveOrCopy.vue';
|
import BatchMoveOrCopy from './batchMoveOrCopy.vue';
|
||||||
import ScheduledModal from './scheduledModal.vue';
|
import ScheduledModal from './scheduledModal.vue';
|
||||||
import StatusProgress from './statusProgress.vue';
|
import StatusProgress from './statusProgress.vue';
|
||||||
|
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addTestPlan,
|
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 = [
|
const batchCopyActions = [
|
||||||
{
|
{
|
||||||
label: 'common.copy',
|
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>>({});
|
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
|
||||||
function getFunctionalCount(id: string) {
|
function getFunctionalCount(id: string) {
|
||||||
return defaultCountDetailMap.value[id]?.caseTotal ?? 0;
|
return defaultCountDetailMap.value[id]?.caseTotal ?? 0;
|
||||||
|
@ -779,11 +754,15 @@
|
||||||
? []
|
? []
|
||||||
: archiveActions;
|
: archiveActions;
|
||||||
|
|
||||||
|
const reportAction =
|
||||||
|
planStatus !== 'ARCHIVED' && record.type === testPlanTypeEnum.GROUP ? [...configReportActions] : [];
|
||||||
|
|
||||||
// 已归档和已完成不展示归档
|
// 已归档和已完成不展示归档
|
||||||
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
|
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
|
||||||
return [
|
return [
|
||||||
...copyAction,
|
...copyAction,
|
||||||
...scheduledTaskAction,
|
...scheduledTaskAction,
|
||||||
|
...reportAction,
|
||||||
{
|
{
|
||||||
label: 'common.delete',
|
label: 'common.delete',
|
||||||
danger: true,
|
danger: true,
|
||||||
|
@ -796,6 +775,7 @@
|
||||||
...copyAction,
|
...copyAction,
|
||||||
...archiveAction,
|
...archiveAction,
|
||||||
...scheduledTaskAction,
|
...scheduledTaskAction,
|
||||||
|
...reportAction,
|
||||||
{
|
{
|
||||||
isDivider: true,
|
isDivider: true,
|
||||||
},
|
},
|
||||||
|
@ -1316,6 +1296,26 @@
|
||||||
activeRecord.value = cloneDeep(record);
|
activeRecord.value = cloneDeep(record);
|
||||||
showStatusDeleteModal.value = true;
|
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) {
|
async function handleDragChange(params: DragSortParams) {
|
||||||
|
@ -1401,6 +1401,9 @@
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteStatusHandler(record);
|
deleteStatusHandler(record);
|
||||||
break;
|
break;
|
||||||
|
case 'configReport':
|
||||||
|
configReportHandler(record);
|
||||||
|
break;
|
||||||
case 'archive':
|
case 'archive':
|
||||||
archiveHandle(record);
|
archiveHandle(record);
|
||||||
break;
|
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)]';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** *
|
/** *
|
||||||
* 高级检索
|
* 高级检索
|
||||||
|
|
|
@ -24,6 +24,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="markdown-body" style="margin-left: 48px" v-html="item.contentText"></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)]">
|
<div class="ml-[48px] mt-[8px] flex text-[var(--color-text-4)]">
|
||||||
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
|
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
|
||||||
<div>
|
<div>
|
||||||
|
@ -42,37 +51,37 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
|
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
|
||||||
import MsEmpty from '@/components/pure/ms-empty/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 { useI18n } from '@/hooks/useI18n';
|
||||||
import { characterLimit } from '@/utils';
|
import { characterLimit } from '@/utils';
|
||||||
|
|
||||||
import type { ExecuteHistoryItem } from '@/models/testPlan/testPlan';
|
import type { ExecuteHistoryItem, ExecuteHistoryType } from '@/models/testPlan/testPlan';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
caseId: string;
|
loadListFun: (params: ExecuteHistoryType) => Promise<ExecuteHistoryItem[]>;
|
||||||
testPlanCaseId: string;
|
extraParams: ExecuteHistoryType;
|
||||||
|
showStepResult?: boolean; // 是否展示执行步骤
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const executeHistoryList = ref<ExecuteHistoryItem[]>([]);
|
const executeHistoryList = ref<ExecuteHistoryItem[]>([]);
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
async function initList() {
|
async function initList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
executeHistoryList.value = await executeHistory({
|
if (props.loadListFun) {
|
||||||
caseId: props.caseId,
|
executeHistoryList.value = await props.loadListFun({
|
||||||
id: props.testPlanCaseId,
|
...props.extraParams,
|
||||||
testPlanId: route.query.id as string,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -80,16 +89,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
function getStepData(steps: string) {
|
||||||
initList();
|
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(
|
watch(
|
||||||
() => props.caseId,
|
() => props.extraParams.caseId,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
initList();
|
initList();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -193,8 +193,12 @@
|
||||||
/>
|
/>
|
||||||
<ExecutionHistory
|
<ExecutionHistory
|
||||||
v-if="activeTab === 'executionHistory'"
|
v-if="activeTab === 'executionHistory'"
|
||||||
:case-id="activeCaseId"
|
:extra-params="{
|
||||||
:test-plan-case-id="activeId"
|
caseId:activeCaseId,
|
||||||
|
id: activeId,
|
||||||
|
testPlanId: route.query.id as string,
|
||||||
|
}"
|
||||||
|
:load-list-fun="executeHistory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
@ -245,6 +249,7 @@
|
||||||
import { getBugList } from '@/api/modules/bug-management';
|
import { getBugList } from '@/api/modules/bug-management';
|
||||||
import {
|
import {
|
||||||
associateBugToPlan,
|
associateBugToPlan,
|
||||||
|
executeHistory,
|
||||||
getCaseDetail,
|
getCaseDetail,
|
||||||
getPlanDetailFeatureCaseList,
|
getPlanDetailFeatureCaseList,
|
||||||
getTestPlanDetail,
|
getTestPlanDetail,
|
||||||
|
|
|
@ -33,15 +33,17 @@
|
||||||
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
|
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
|
||||||
{{ t('common.edit') }}
|
{{ t('common.edit') }}
|
||||||
</MsButton>
|
</MsButton>
|
||||||
|
<!-- TODO 等待联调 接口需要调整和增加 -->
|
||||||
|
<MsTableMoreAction :list="reportMoreAction" @select="handleMoreReportSelect">
|
||||||
<MsButton
|
<MsButton
|
||||||
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && detail.status !== 'ARCHIVED'"
|
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && detail.status !== 'ARCHIVED'"
|
||||||
type="button"
|
type="button"
|
||||||
status="default"
|
status="default"
|
||||||
@click="handleGenerateReport"
|
|
||||||
>
|
>
|
||||||
<MsIcon type="icon-icon_generate_report" class="mr-[8px]" />
|
<MsIcon type="icon-icon_generate_report" class="mr-[8px]" />
|
||||||
{{ t('testPlan.testPlanDetail.generateReport') }}
|
{{ t('testPlan.testPlanDetail.generateReport') }}
|
||||||
</MsButton>
|
</MsButton>
|
||||||
|
</MsTableMoreAction>
|
||||||
<MsButton
|
<MsButton
|
||||||
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && detail.status !== 'ARCHIVED'"
|
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && detail.status !== 'ARCHIVED'"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -185,6 +187,7 @@
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
import type { PassRateCountDetail, TestPlanDetail, TestPlanItem } from '@/models/testPlan/testPlan';
|
import type { PassRateCountDetail, TestPlanDetail, TestPlanItem } from '@/models/testPlan/testPlan';
|
||||||
import { TestPlanRouteEnum } from '@/enums/routeEnum';
|
import { TestPlanRouteEnum } from '@/enums/routeEnum';
|
||||||
|
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -380,7 +383,7 @@
|
||||||
|
|
||||||
const showPlanDrawer = ref(false);
|
const showPlanDrawer = ref(false);
|
||||||
|
|
||||||
// 生成报告
|
// 生成报告 TODO 等待联调 后台要改接口
|
||||||
async function handleGenerateReport() {
|
async function handleGenerateReport() {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
@ -397,6 +400,52 @@
|
||||||
loading.value = false;
|
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);
|
const isCopy = ref<boolean>(false);
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<template #tbutton>
|
<template #tbutton>
|
||||||
<PlanDetailHeaderRight share-id="" :detail="detail" />
|
<PlanDetailHeaderRight share-id="" :detail="detail" />
|
||||||
</template>
|
</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>
|
</MsDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,13 +22,15 @@
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
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/system-card/planDetailHeaderRight.vue';
|
||||||
import PlanDetailHeaderRight from '@/views/test-plan/report/detail/component/planDetailHeaderRight.vue';
|
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
|
||||||
|
|
||||||
import { getReportDetail } from '@/api/modules/test-plan/report';
|
import { getReportDetail } from '@/api/modules/test-plan/report';
|
||||||
import { defaultReportDetail } from '@/config/testPlan';
|
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<{
|
const props = defineProps<{
|
||||||
reportId: string;
|
reportId: string;
|
||||||
|
@ -40,6 +42,8 @@
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const cardItemList = ref<configItem[]>(cloneDeep(defaultSingleConfig));
|
||||||
|
|
||||||
const shareId = ref<string>(route.query.shareId as string);
|
const shareId = ref<string>(route.query.shareId as string);
|
||||||
|
|
||||||
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
||||||
|
|
|
@ -144,5 +144,7 @@ export default {
|
||||||
'testPlan.plan': 'Test plan',
|
'testPlan.plan': 'Test plan',
|
||||||
'testPlan.planTip':
|
'testPlan.planTip':
|
||||||
'1. Create a test set for business classification testing; 2. Select the test set associated use case',
|
'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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -135,4 +135,6 @@ export default {
|
||||||
'testPlan.plan': '测试规划',
|
'testPlan.plan': '测试规划',
|
||||||
'testPlan.planTip': '1.创建测试点进行业务分类测试;2.选择测试点关联用例',
|
'testPlan.planTip': '1.创建测试点进行业务分类测试;2.选择测试点关联用例',
|
||||||
'testPlan.planStartToEndTimeTip': '测试计划已超时',
|
'testPlan.planStartToEndTimeTip': '测试计划已超时',
|
||||||
|
'testPlan.planConfigReport': '自定义报告',
|
||||||
|
'testPlan.planAutomaticGeneration': '自动生成',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue