feat(测试计划): 测试计划详情里功能用例列表
This commit is contained in:
parent
ce0ed0711e
commit
bfbd4b138c
|
@ -8,6 +8,9 @@ import {
|
||||||
batchMovePlanUrl,
|
batchMovePlanUrl,
|
||||||
deletePlanUrl,
|
deletePlanUrl,
|
||||||
DeleteTestPlanModuleUrl,
|
DeleteTestPlanModuleUrl,
|
||||||
|
GetFeatureCaseModuleCountUrl,
|
||||||
|
GetFeatureCaseModuleUrl,
|
||||||
|
GetPlanDetailFeatureCaseListUrl,
|
||||||
getStatisticalCountUrl,
|
getStatisticalCountUrl,
|
||||||
GetTestPlanDetailUrl,
|
GetTestPlanDetailUrl,
|
||||||
GetTestPlanListUrl,
|
GetTestPlanListUrl,
|
||||||
|
@ -25,6 +28,8 @@ import { ModuleTreeNode } from '@/models/common';
|
||||||
import type {
|
import type {
|
||||||
AddTestPlanParams,
|
AddTestPlanParams,
|
||||||
PlanDetailBugItem,
|
PlanDetailBugItem,
|
||||||
|
PlanDetailFeatureCaseItem,
|
||||||
|
PlanDetailFeatureCaseListQueryParams,
|
||||||
TestPlanDetail,
|
TestPlanDetail,
|
||||||
TestPlanItem,
|
TestPlanItem,
|
||||||
UseCountType,
|
UseCountType,
|
||||||
|
@ -107,3 +112,15 @@ export function batchMovePlan(data: TableQueryParams) {
|
||||||
export function planDetailBugPage(data: TableQueryParams) {
|
export function planDetailBugPage(data: TableQueryParams) {
|
||||||
return MSR.post<CommonList<PlanDetailBugItem>>({ url: planDetailBugPageUrl, data });
|
return MSR.post<CommonList<PlanDetailBugItem>>({ url: planDetailBugPageUrl, data });
|
||||||
}
|
}
|
||||||
|
// 计划详情-功能用例列表
|
||||||
|
export function getPlanDetailFeatureCaseList(data: PlanDetailFeatureCaseListQueryParams) {
|
||||||
|
return MSR.post<CommonList<PlanDetailFeatureCaseItem>>({ url: GetPlanDetailFeatureCaseListUrl, data });
|
||||||
|
}
|
||||||
|
// 计划详情-功能用例-获取模块数量
|
||||||
|
export function getFeatureCaseModuleCount(data: PlanDetailFeatureCaseListQueryParams) {
|
||||||
|
return MSR.post({ url: GetFeatureCaseModuleCountUrl, data });
|
||||||
|
}
|
||||||
|
// 计划详情-功能用例模块树
|
||||||
|
export function getFeatureCaseModule(planId: string) {
|
||||||
|
return MSR.get<ModuleTreeNode[]>({ url: `${GetFeatureCaseModuleUrl}/${planId}` });
|
||||||
|
}
|
||||||
|
|
|
@ -32,3 +32,9 @@ export const batchCopyPlanUrl = '/test-plan/batch/copy';
|
||||||
export const batchMovePlanUrl = '/test-plan/batch/move';
|
export const batchMovePlanUrl = '/test-plan/batch/move';
|
||||||
// 计划详情缺陷管理列表
|
// 计划详情缺陷管理列表
|
||||||
export const planDetailBugPageUrl = '/test-plan/bug/page';
|
export const planDetailBugPageUrl = '/test-plan/bug/page';
|
||||||
|
// 计划详情-功能用例列表
|
||||||
|
export const GetPlanDetailFeatureCaseListUrl = '/test-plan/functional/case/page';
|
||||||
|
// 计划详情-功能用例-获取模块数量
|
||||||
|
export const GetFeatureCaseModuleCountUrl = '/test-plan/functional/case/module/count';
|
||||||
|
// 计划详情-功能用例模块树
|
||||||
|
export const GetFeatureCaseModuleUrl = '/test-plan/functional/case/tree';
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
PASSED: {
|
PASSED: {
|
||||||
label: 'PASSED',
|
label: 'PASSED',
|
||||||
icon: StatusType.PASSED,
|
icon: StatusType.PASSED,
|
||||||
statusText: 'caseManagement.featureCase.passed',
|
statusText: 'common.success',
|
||||||
color: '',
|
color: '',
|
||||||
},
|
},
|
||||||
SKIPPED: {
|
SKIPPED: {
|
||||||
|
|
|
@ -840,7 +840,7 @@
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 1;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ export enum TableKeyEnum {
|
||||||
PROJECT_MANAGEMENT_ENV_ALL_PARAM_VARIABLE = 'projectManagementEnvAllParamVariable',
|
PROJECT_MANAGEMENT_ENV_ALL_PARAM_VARIABLE = 'projectManagementEnvAllParamVariable',
|
||||||
TEST_PLAN_ALL_TABLE = 'testPlanAllTable',
|
TEST_PLAN_ALL_TABLE = 'testPlanAllTable',
|
||||||
TEST_PLAN_DETAIL_BUG_TABLE = 'testPlanDetailBug',
|
TEST_PLAN_DETAIL_BUG_TABLE = 'testPlanDetailBug',
|
||||||
|
TEST_PLAN_DETAIL_FEATURE_CASE_TABLE = 'testPlanDetailFeatureCaseTable',
|
||||||
TEST_PLAN_DETAIL_BUG_TABLE_CASE_COUNT = 'testPlanDetailBugCaseCount',
|
TEST_PLAN_DETAIL_BUG_TABLE_CASE_COUNT = 'testPlanDetailBugCaseCount',
|
||||||
TASK_API_CASE_SYSTEM = 'taskCenterApiCaseSystem',
|
TASK_API_CASE_SYSTEM = 'taskCenterApiCaseSystem',
|
||||||
TASK_API_CASE_ORGANIZATION = 'taskCenterApiCaseOrganization',
|
TASK_API_CASE_ORGANIZATION = 'taskCenterApiCaseOrganization',
|
||||||
|
|
|
@ -40,6 +40,7 @@ export default {
|
||||||
'common.saveSuccess': '保存成功',
|
'common.saveSuccess': '保存成功',
|
||||||
'common.saveFailed': '保存失败',
|
'common.saveFailed': '保存失败',
|
||||||
'common.linkSuccess': '关联成功',
|
'common.linkSuccess': '关联成功',
|
||||||
|
'common.cancelLink': '取消关联',
|
||||||
'common.unLinkSuccess': '取消关联成功',
|
'common.unLinkSuccess': '取消关联成功',
|
||||||
'common.confirmEnable': '确认启用',
|
'common.confirmEnable': '确认启用',
|
||||||
'common.confirmDisable': '确认禁用',
|
'common.confirmDisable': '确认禁用',
|
||||||
|
@ -169,4 +170,5 @@ export default {
|
||||||
'common.unPass': '不通过',
|
'common.unPass': '不通过',
|
||||||
'common.belongModule': '所属模块',
|
'common.belongModule': '所属模块',
|
||||||
'common.moreSetting': '更多设置',
|
'common.moreSetting': '更多设置',
|
||||||
|
'common.executionResult': '执行结果',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import type { customFieldsItem } from '@/models/caseManagement/featureCase';
|
||||||
|
import type { TableQueryParams } from '@/models/common';
|
||||||
|
|
||||||
import { BatchApiParams } from '../common';
|
import { BatchApiParams } from '../common';
|
||||||
|
|
||||||
export type planStatusType = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED';
|
export type planStatusType = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED';
|
||||||
|
@ -122,4 +125,27 @@ export interface FollowPlanParams {
|
||||||
testPlanId: string;
|
testPlanId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanDetailFeatureCaseItem {
|
||||||
|
id: string;
|
||||||
|
num: string;
|
||||||
|
name: string;
|
||||||
|
moduleId: string;
|
||||||
|
versionName: string;
|
||||||
|
createUser: string;
|
||||||
|
createUserName: string;
|
||||||
|
lastExecResult: string;
|
||||||
|
lastExecTime: number;
|
||||||
|
executeUser: string;
|
||||||
|
executeUserName: string;
|
||||||
|
bugCount: number;
|
||||||
|
customFields: customFieldsItem[]; // 自定义字段集合
|
||||||
|
caseId: string;
|
||||||
|
testPlanId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanDetailFeatureCaseListQueryParams extends TableQueryParams {
|
||||||
|
testPlanId: string;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default {};
|
export default {};
|
||||||
|
|
|
@ -5,9 +5,11 @@ import type { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { findNodePathByKey } from '@/utils';
|
||||||
import { hasAnyPermission } from '@/utils/permission';
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
import type { AssociatedList, CustomAttributes } from '@/models/caseManagement/featureCase';
|
import type { AssociatedList, CustomAttributes } from '@/models/caseManagement/featureCase';
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
import { StatusType } from '@/enums/caseEnum';
|
import { StatusType } from '@/enums/caseEnum';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -62,7 +64,7 @@ export const executionResultMap: Record<string, any> = {
|
||||||
PASSED: {
|
PASSED: {
|
||||||
key: 'PASSED',
|
key: 'PASSED',
|
||||||
icon: StatusType.PASSED,
|
icon: StatusType.PASSED,
|
||||||
statusText: t('caseManagement.featureCase.passed'),
|
statusText: t('common.success'),
|
||||||
color: '',
|
color: '',
|
||||||
},
|
},
|
||||||
/* SKIPPED: {
|
/* SKIPPED: {
|
||||||
|
@ -130,6 +132,18 @@ export function getCaseLevels(customFields: CustomAttributes[]): CaseLevel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取对应模块name
|
||||||
|
export function getModules(moduleIds: string, treeData: ModuleTreeNode[]) {
|
||||||
|
const modules = findNodePathByKey(treeData, moduleIds, undefined, 'id');
|
||||||
|
if (modules) {
|
||||||
|
const moduleName = (modules || [])?.treePath.map((item: any) => item.name);
|
||||||
|
if (moduleName.length === 1) {
|
||||||
|
return moduleName[0];
|
||||||
|
}
|
||||||
|
return `/${moduleName.join('/')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理自定义字段
|
// 处理自定义字段
|
||||||
export function getTableFields(customFields: CustomAttributes[], itemDataIndex: MsTableColumnData, userId: string) {
|
export function getTableFields(customFields: CustomAttributes[], itemDataIndex: MsTableColumnData, userId: string) {
|
||||||
const multipleExcludes = ['MULTIPLE_SELECT', 'CHECKBOX', 'MULTIPLE_MEMBER'];
|
const multipleExcludes = ['MULTIPLE_SELECT', 'CHECKBOX', 'MULTIPLE_MEMBER'];
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
function saveHandler(params: AssociateCaseRequest) {
|
function saveHandler(params: AssociateCaseRequest) {
|
||||||
try {
|
try {
|
||||||
confirmLoading.value = true;
|
confirmLoading.value = true;
|
||||||
emit('success', { ...params });
|
emit('success', { ...params, functionalSelectIds: params.selectIds });
|
||||||
innerVisible.value = false;
|
innerVisible.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
|
@ -0,0 +1,332 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-[16px]">
|
||||||
|
<div class="mb-[16px] flex justify-end">
|
||||||
|
<a-input-search
|
||||||
|
v-model:model-value="keyword"
|
||||||
|
:placeholder="t('ms.case.associate.searchPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
class="mr-[8px] w-[240px]"
|
||||||
|
@search="loadCaseList"
|
||||||
|
@press-enter="loadCaseList"
|
||||||
|
@clear="loadCaseList"
|
||||||
|
/>
|
||||||
|
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]" @click="loadCaseList">
|
||||||
|
<template #icon>
|
||||||
|
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<MsBaseTable v-bind="propsRes" :action-config="batchActions" v-on="propsEvent" @batch-action="handleTableBatch">
|
||||||
|
<template #num="{ record }">
|
||||||
|
<MsButton type="text">{{ record.num }}</MsButton>
|
||||||
|
</template>
|
||||||
|
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
|
||||||
|
<CaseLevel :case-level="filterContent.value" />
|
||||||
|
</template>
|
||||||
|
<template #caseLevel="{ record }">
|
||||||
|
<CaseLevel :case-level="record.caseLevel" />
|
||||||
|
</template>
|
||||||
|
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
|
||||||
|
<ExecuteResult :execute-result="filterContent.key" />
|
||||||
|
</template>
|
||||||
|
<template #lastExecResult="{ record }">
|
||||||
|
<!-- TODO: 修改permission -->
|
||||||
|
<a-select
|
||||||
|
v-if="hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ+UPDATE'])"
|
||||||
|
v-model:model-value="record.lastExecResult"
|
||||||
|
:placeholder="t('common.pleaseSelect')"
|
||||||
|
class="param-input w-full"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-[var(--color-text-2)]"><ExecuteResult :execute-result="record.lastExecResult" /></span>
|
||||||
|
</template>
|
||||||
|
<a-option v-for="item in Object.values(executionResultMap)" :key="item.key" :value="item.key">
|
||||||
|
<ExecuteResult :execute-result="item.key" />
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
<span v-else class="text-[var(--color-text-2)]"><ExecuteResult :execute-result="record.lastExecResult" /></span>
|
||||||
|
</template>
|
||||||
|
<template #operation>
|
||||||
|
<MsButton v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']" type="text" class="!mr-0">
|
||||||
|
{{ t('common.execute') }}
|
||||||
|
</MsButton>
|
||||||
|
<a-divider v-permission="['PROJECT_TEST_PLAN:READ+ASSOCIATION']" direction="vertical" :margin="8"></a-divider>
|
||||||
|
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+ASSOCIATION']" type="text" class="!mr-0">
|
||||||
|
{{ t('common.cancelLink') }}
|
||||||
|
</MsButton>
|
||||||
|
<!-- TODO: 修改permission -->
|
||||||
|
<a-divider
|
||||||
|
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
|
||||||
|
direction="vertical"
|
||||||
|
:margin="8"
|
||||||
|
></a-divider>
|
||||||
|
<MsButton v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']" type="text" class="!mr-0">
|
||||||
|
{{ t('common.copy') }}
|
||||||
|
</MsButton>
|
||||||
|
</template>
|
||||||
|
</MsBaseTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeMount, ref } from 'vue';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
|
import type { BatchActionParams, BatchActionQueryParams, 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 { getPlanDetailFeatureCaseList } from '@/api/modules/test-plan/testPlan';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useTableStore from '@/hooks/useTableStore';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
import type { PlanDetailFeatureCaseListQueryParams } from '@/models/testPlan/testPlan';
|
||||||
|
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||||
|
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||||
|
|
||||||
|
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||||
|
import {
|
||||||
|
executionResultMap,
|
||||||
|
getCaseLevels,
|
||||||
|
getModules,
|
||||||
|
} from '@/views/case-management/caseManagementFeature/components/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeModule: string;
|
||||||
|
offspringIds: string[];
|
||||||
|
planId: string;
|
||||||
|
moduleTree: ModuleTreeNode[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'init', params: PlanDetailFeatureCaseListQueryParams): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const tableStore = useTableStore();
|
||||||
|
|
||||||
|
const keyword = ref('');
|
||||||
|
|
||||||
|
// TODO: 复制的Permission
|
||||||
|
const hasOperationPermission = computed(() =>
|
||||||
|
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN:READ+ASSOCIATION'])
|
||||||
|
);
|
||||||
|
const columns: MsTableColumn = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'num',
|
||||||
|
slotName: 'num',
|
||||||
|
sortIndex: 1,
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
fixed: 'left',
|
||||||
|
width: 100,
|
||||||
|
ellipsis: true,
|
||||||
|
showTooltip: true,
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'case.caseName',
|
||||||
|
dataIndex: 'name',
|
||||||
|
showTooltip: true,
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
width: 180,
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'case.caseLevel',
|
||||||
|
dataIndex: 'caseLevel',
|
||||||
|
slotName: 'caseLevel',
|
||||||
|
filterConfig: {
|
||||||
|
options: casePriorityOptions,
|
||||||
|
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
|
||||||
|
},
|
||||||
|
width: 150,
|
||||||
|
showDrag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.executionResult',
|
||||||
|
dataIndex: 'lastExecResult',
|
||||||
|
slotName: 'lastExecResult',
|
||||||
|
filterConfig: {
|
||||||
|
valueKey: 'key',
|
||||||
|
labelKey: 'statusText',
|
||||||
|
options: Object.values(executionResultMap),
|
||||||
|
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
|
||||||
|
},
|
||||||
|
width: 150,
|
||||||
|
showDrag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.belongModule',
|
||||||
|
dataIndex: 'moduleId',
|
||||||
|
ellipsis: true,
|
||||||
|
showTooltip: true,
|
||||||
|
width: 200,
|
||||||
|
showDrag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'testPlan.featureCase.bugCount',
|
||||||
|
dataIndex: 'bugCount',
|
||||||
|
width: 100,
|
||||||
|
showDrag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'case.tableColumnCreateUser',
|
||||||
|
dataIndex: 'createUserName',
|
||||||
|
showTooltip: true,
|
||||||
|
width: 150,
|
||||||
|
showDrag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'testPlan.featureCase.executor',
|
||||||
|
dataIndex: 'executeUserName',
|
||||||
|
width: 150,
|
||||||
|
showDrag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: hasOperationPermission.value ? 'common.operation' : '',
|
||||||
|
slotName: 'operation',
|
||||||
|
dataIndex: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
width: hasOperationPermission.value ? 200 : 50,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||||
|
getPlanDetailFeatureCaseList,
|
||||||
|
{
|
||||||
|
scroll: { x: '100%' },
|
||||||
|
tableKey: TableKeyEnum.TEST_PLAN_DETAIL_FEATURE_CASE_TABLE,
|
||||||
|
showSetting: true,
|
||||||
|
selectable: hasOperationPermission.value,
|
||||||
|
showSelectAll: true,
|
||||||
|
draggable: hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) ? { type: 'handle', width: 32 } : undefined,
|
||||||
|
heightUsed: 460,
|
||||||
|
showSubdirectory: true,
|
||||||
|
},
|
||||||
|
(record) => {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
caseLevel: getCaseLevels(record.customFields),
|
||||||
|
moduleId: getModules(record.moduleId, props.moduleTree),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// TODO: permission
|
||||||
|
const batchActions = {
|
||||||
|
baseAction: [
|
||||||
|
{
|
||||||
|
label: 'common.execute',
|
||||||
|
eventTag: 'execute',
|
||||||
|
permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'testPlan.featureCase.sort',
|
||||||
|
eventTag: 'sort',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'testPlan.featureCase.changeExecutor',
|
||||||
|
eventTag: 'changeExecutor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
moreAction: [
|
||||||
|
{
|
||||||
|
label: 'common.cancelLink',
|
||||||
|
eventTag: 'disassociate',
|
||||||
|
permission: ['PROJECT_TEST_PLAN:READ+ASSOCIATION'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common.delete',
|
||||||
|
eventTag: 'delete',
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableSelected = ref<(string | number)[]>([]); // 表格选中的
|
||||||
|
const batchParams = ref<BatchActionQueryParams>({
|
||||||
|
selectedIds: [],
|
||||||
|
selectAll: false,
|
||||||
|
excludeIds: [],
|
||||||
|
currentSelectCount: 0,
|
||||||
|
});
|
||||||
|
// 处理表格选中后批量操作
|
||||||
|
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
|
||||||
|
tableSelected.value = params?.selectedIds || [];
|
||||||
|
batchParams.value = params;
|
||||||
|
switch (event.eventTag) {
|
||||||
|
case 'execute':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getModuleIds() {
|
||||||
|
let moduleIds: string[] = [];
|
||||||
|
if (props.activeModule !== 'all') {
|
||||||
|
moduleIds = [props.activeModule];
|
||||||
|
const getAllChildren = await tableStore.getSubShow(TableKeyEnum.TEST_PLAN_DETAIL_FEATURE_CASE_TABLE);
|
||||||
|
if (getAllChildren) {
|
||||||
|
moduleIds = [props.activeModule, ...props.offspringIds];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return moduleIds;
|
||||||
|
}
|
||||||
|
async function loadCaseList() {
|
||||||
|
const selectModules = await getModuleIds();
|
||||||
|
const params: PlanDetailFeatureCaseListQueryParams = {
|
||||||
|
testPlanId: props.planId,
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
moduleIds: selectModules,
|
||||||
|
keyword: keyword.value,
|
||||||
|
};
|
||||||
|
setLoadListParams(params);
|
||||||
|
loadList();
|
||||||
|
emit('init', {
|
||||||
|
...params,
|
||||||
|
current: propsRes.value.msPagination?.current,
|
||||||
|
pageSize: propsRes.value.msPagination?.pageSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onBeforeMount(() => {
|
||||||
|
loadCaseList();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
resetSelector,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_DETAIL_FEATURE_CASE_TABLE, columns, 'drawer', true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
|
||||||
|
&:not(:hover) {
|
||||||
|
border-color: transparent !important;
|
||||||
|
.arco-input::placeholder {
|
||||||
|
@apply invisible;
|
||||||
|
}
|
||||||
|
.arco-select-view-icon {
|
||||||
|
@apply invisible;
|
||||||
|
}
|
||||||
|
.arco-select-view-value {
|
||||||
|
color: var(--color-text-brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,203 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="moduleKeyword"
|
||||||
|
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
class="mb-[8px]"
|
||||||
|
:max-length="255"
|
||||||
|
/>
|
||||||
|
<div class="folder">
|
||||||
|
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
|
||||||
|
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||||
|
<div class="folder-name">{{ t('caseManagement.caseReview.allCases') }}</div>
|
||||||
|
<div class="folder-count">({{ allCount }})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-divider class="my-[8px]" />
|
||||||
|
<a-spin class="min-h-[200px] w-full" :loading="loading">
|
||||||
|
<MsTree
|
||||||
|
:selected-keys="selectedKeys"
|
||||||
|
:data="folderTree"
|
||||||
|
:keyword="moduleKeyword"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:expand-all="isExpandAll"
|
||||||
|
:empty-text="t('caseManagement.caseReview.noCases')"
|
||||||
|
:draggable="false"
|
||||||
|
:virtual-list-props="virtualListProps"
|
||||||
|
:field-names="{
|
||||||
|
title: 'name',
|
||||||
|
key: 'id',
|
||||||
|
children: 'children',
|
||||||
|
count: 'count',
|
||||||
|
}"
|
||||||
|
block-node
|
||||||
|
title-tooltip-position="left"
|
||||||
|
@select="folderNodeSelect"
|
||||||
|
>
|
||||||
|
<template #title="nodeData">
|
||||||
|
<div class="inline-flex w-full gap-[8px]">
|
||||||
|
<div class="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||||
|
<div class="ms-tree-node-count ml-[4px] text-[var(--color-text-brand)]">{{ nodeData.count || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsTree>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
|
||||||
|
import { getFeatureCaseModule } from '@/api/modules/test-plan/testPlan';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { mapTree } from '@/utils';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||||
|
isExpandAll?: boolean; // 是否展开所有节点
|
||||||
|
selectedKeys: string[]; // 选中的节点 key
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'folderNodeSelect', ids: string[], _offspringIds: string[]): void;
|
||||||
|
(e: 'init', params: ModuleTreeNode[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const virtualListProps = computed(() => {
|
||||||
|
return {
|
||||||
|
height: 'calc(100vh - 408px)',
|
||||||
|
threshold: 200,
|
||||||
|
fixedSize: true,
|
||||||
|
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeFolder = ref<string>('all');
|
||||||
|
const allCount = ref(0);
|
||||||
|
const isExpandAll = ref(props.isExpandAll);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isExpandAll,
|
||||||
|
(val) => {
|
||||||
|
isExpandAll.value = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function getFolderClass(id: string) {
|
||||||
|
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveFolder(id: string) {
|
||||||
|
activeFolder.value = id;
|
||||||
|
emit('folderNodeSelect', [id], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleKeyword = ref('');
|
||||||
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const selectedKeys = useVModel(props, 'selectedKeys', emit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模块树
|
||||||
|
*/
|
||||||
|
async function initModules() {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getFeatureCaseModule(route.query.id as string);
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(res, (node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
count: props.modulesCount?.[node.id] || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
emit('init', folderTree.value);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件夹树节点选中事件
|
||||||
|
*/
|
||||||
|
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
|
||||||
|
const offspringIds: string[] = [];
|
||||||
|
mapTree(node.children || [], (e) => {
|
||||||
|
offspringIds.push(e.id);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
activeFolder.value = node.id;
|
||||||
|
emit('folderNodeSelect', _selectedKeys as string[], offspringIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模块文件数量
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.modulesCount,
|
||||||
|
(obj) => {
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
count: obj?.[node.id] || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
allCount.value = obj?.all || 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
initModules,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.folder {
|
||||||
|
@apply flex cursor-pointer items-center justify-between;
|
||||||
|
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(var(--primary-1));
|
||||||
|
}
|
||||||
|
.folder-text {
|
||||||
|
@apply flex cursor-pointer items-center;
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.folder-name {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
.folder-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.folder-text--active {
|
||||||
|
.folder-icon,
|
||||||
|
.folder-name,
|
||||||
|
.folder-count {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<MsSplitBox>
|
||||||
|
<template #first>
|
||||||
|
<div class="p-[16px]">
|
||||||
|
<CaseTree
|
||||||
|
:modules-count="modulesCount"
|
||||||
|
:selected-keys="selectedKeys"
|
||||||
|
@folder-node-select="handleFolderNodeSelect"
|
||||||
|
@init="initModuleTree"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #second>
|
||||||
|
<CaseTable
|
||||||
|
ref="caseTableRef"
|
||||||
|
:plan-id="planId"
|
||||||
|
:active-module="activeFolderId"
|
||||||
|
:offspring-ids="offspringIds"
|
||||||
|
:module-tree="moduleTree"
|
||||||
|
@init="getModuleCount"
|
||||||
|
></CaseTable>
|
||||||
|
</template>
|
||||||
|
</MsSplitBox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
|
import CaseTable from './components/caseTable.vue';
|
||||||
|
import CaseTree from './components/caseTree.vue';
|
||||||
|
|
||||||
|
import { getFeatureCaseModuleCount } from '@/api/modules/test-plan/testPlan';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
import type { PlanDetailFeatureCaseListQueryParams } from '@/models/testPlan/testPlan';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const planId = ref(route.query.id as string);
|
||||||
|
const modulesCount = ref<Record<string, any>>({});
|
||||||
|
async function getModuleCount(params: PlanDetailFeatureCaseListQueryParams) {
|
||||||
|
try {
|
||||||
|
modulesCount.value = await getFeatureCaseModuleCount(params);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const caseTableRef = ref<InstanceType<typeof CaseTable>>();
|
||||||
|
const activeFolderId = ref<string>('all');
|
||||||
|
const offspringIds = ref<string[]>([]);
|
||||||
|
const selectedKeys = computed({
|
||||||
|
get: () => [activeFolderId.value],
|
||||||
|
set: (val) => val,
|
||||||
|
});
|
||||||
|
function handleFolderNodeSelect(ids: string[], _offspringIds: string[]) {
|
||||||
|
[activeFolderId.value] = ids;
|
||||||
|
offspringIds.value = [..._offspringIds];
|
||||||
|
caseTableRef.value?.resetSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
function initModuleTree(tree: ModuleTreeNode[]) {
|
||||||
|
moduleTree.value = unref(tree);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -73,6 +73,7 @@
|
||||||
</MsCard>
|
</MsCard>
|
||||||
<!-- special-height的174: 上面卡片高度158 + mt的16 -->
|
<!-- special-height的174: 上面卡片高度158 + mt的16 -->
|
||||||
<MsCard class="mt-[16px]" :special-height="174" simple has-breadcrumb no-content-padding>
|
<MsCard class="mt-[16px]" :special-height="174" simple has-breadcrumb no-content-padding>
|
||||||
|
<FeatureCase v-if="activeTab === 'featureCase'" />
|
||||||
<BugManagement v-if="activeTab === 'defectList'" :plan-id="detail.id" />
|
<BugManagement v-if="activeTab === 'defectList'" :plan-id="detail.id" />
|
||||||
</MsCard>
|
</MsCard>
|
||||||
</template>
|
</template>
|
||||||
|
@ -88,6 +89,7 @@
|
||||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
|
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
|
||||||
import BugManagement from './bugManagement/index.vue';
|
import BugManagement from './bugManagement/index.vue';
|
||||||
|
import FeatureCase from './featureCase/index.vue';
|
||||||
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
|
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
|
||||||
|
|
||||||
import { getTestPlanDetail } from '@/api/modules/test-plan/testPlan';
|
import { getTestPlanDetail } from '@/api/modules/test-plan/testPlan';
|
||||||
|
|
|
@ -85,4 +85,8 @@ export default {
|
||||||
'testPlan.bugManagement.bugName': 'name',
|
'testPlan.bugManagement.bugName': 'name',
|
||||||
'testPlan.bugManagement.defectState': 'Defect state',
|
'testPlan.bugManagement.defectState': 'Defect state',
|
||||||
'testPlan.bugManagement.caseClassification': 'Classification',
|
'testPlan.bugManagement.caseClassification': 'Classification',
|
||||||
|
'testPlan.featureCase.bugCount': 'Bug count',
|
||||||
|
'testPlan.featureCase.executor': 'Executor',
|
||||||
|
'testPlan.featureCase.changeExecutor': 'Change executor',
|
||||||
|
'testPlan.featureCase.sort': 'sort',
|
||||||
};
|
};
|
||||||
|
|
|
@ -83,4 +83,8 @@ export default {
|
||||||
'testPlan.bugManagement.bugName': '名称',
|
'testPlan.bugManagement.bugName': '名称',
|
||||||
'testPlan.bugManagement.defectState': '缺陷状态',
|
'testPlan.bugManagement.defectState': '缺陷状态',
|
||||||
'testPlan.bugManagement.caseClassification': '用例分类',
|
'testPlan.bugManagement.caseClassification': '用例分类',
|
||||||
|
'testPlan.featureCase.bugCount': '缺陷数',
|
||||||
|
'testPlan.featureCase.executor': '执行人',
|
||||||
|
'testPlan.featureCase.changeExecutor': '修改执行人',
|
||||||
|
'testPlan.featureCase.sort': '排序',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue