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