feat(测试计划): 测试计划模块和测试计划列表静态页面

This commit is contained in:
xinxin.wu 2024-05-07 15:01:12 +08:00 committed by 刘瑞斌
parent b642981f2f
commit 1ceb194def
24 changed files with 1720 additions and 644 deletions

View File

@ -2,17 +2,21 @@ import MSR from '@/api/http/index';
import { import {
addTestPlanModuleUrl, addTestPlanModuleUrl,
DeleteTestPlanModuleUrl, DeleteTestPlanModuleUrl,
GetTestPlanListUrl,
GetTestPlanModuleCountUrl, GetTestPlanModuleCountUrl,
GetTestPlanModuleUrl, GetTestPlanModuleUrl,
MoveTestPlanModuleUrl, MoveTestPlanModuleUrl,
updateTestPlanModuleUrl, updateTestPlanModuleUrl,
} from '@/api/requrls/test-plan/testPlan'; } from '@/api/requrls/test-plan/testPlan';
import type { CreateOrUpdateModule, ModulesTreeType, UpdateModule } from '@/models/caseManagement/featureCase'; import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common'; import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common';
import type { TestPlanItem } from '@/models/testPlan/testPlan';
// 获取模块树 // 获取模块树
export function getTestPlanModule(params: TableQueryParams) { export function getTestPlanModule(params: TableQueryParams) {
return MSR.get<ModulesTreeType[]>({ url: `${GetTestPlanModuleUrl}/${params.projectId}` }); return MSR.get<ModuleTreeNode[]>({ url: `${GetTestPlanModuleUrl}/${params.projectId}` });
} }
// 创建模块树 // 创建模块树
@ -39,3 +43,8 @@ export function deletePlanModuleTree(id: string) {
export function getPlanModulesCounts(data: TableQueryParams) { export function getPlanModulesCounts(data: TableQueryParams) {
return MSR.post({ url: GetTestPlanModuleCountUrl, data }); return MSR.post({ url: GetTestPlanModuleCountUrl, data });
} }
// 获取计划列表
export function getTestPlanList(data: TableQueryParams) {
return MSR.post<CommonList<TestPlanItem>>({ url: GetTestPlanListUrl, data });
}

View File

@ -10,3 +10,5 @@ export const MoveTestPlanModuleUrl = '/test-plan/module/move';
export const DeleteTestPlanModuleUrl = '/test-plan/module/delete'; export const DeleteTestPlanModuleUrl = '/test-plan/module/delete';
// 测试计划模块树数量 // 测试计划模块树数量
export const GetTestPlanModuleCountUrl = '/test-plan/module/count'; export const GetTestPlanModuleCountUrl = '/test-plan/module/count';
// 测试计划列表
export const GetTestPlanListUrl = '/test-plan/page';

View File

@ -80,8 +80,21 @@
emit('change', v as SelectAllEnum); emit('change', v as SelectAllEnum);
}; };
function hasUnselectedChildren(
data: MsTableDataItem<Record<string, any>>[],
selectedKeys: Set<string>,
rowKey: string
): boolean {
return data.some((item: any) => {
if (item.children && item.children.length > 0) {
return hasUnselectedChildren(item.children, selectedKeys, rowKey);
}
return !selectedKeys.has(item[rowKey]);
});
}
const handleCheckChange = () => { const handleCheckChange = () => {
if (props.currentData.some((item) => !props.selectedKeys.has(item[props.rowKey]))) { if (hasUnselectedChildren(props.currentData, props.selectedKeys, props.rowKey)) {
// //
handleSelect(SelectAllEnum.CURRENT); handleSelect(SelectAllEnum.CURRENT);
} else { } else {

View File

@ -66,7 +66,7 @@
const getTagWidth = (tag: { [x: string]: any }) => { const getTagWidth = (tag: { [x: string]: any }) => {
const tagStr = props.isStringTag ? tag : tag[props.nameKey]; const tagStr = props.isStringTag ? tag : tag[props.nameKey];
const tagWidth = tagStr.length; const tagWidth = tagStr ? tagStr.length : 0;
// 16 // 16
return tagWidth < 16 ? tagWidth : 16; return tagWidth < 16 ? tagWidth : 16;
}; };

View File

@ -0,0 +1,7 @@
export enum testPlanTypeEnum {
ALL = 'ALL',
TEST_PLAN = 'TEST_PLAN',
GROUP = 'GROUP',
}
export default {};

View File

@ -122,6 +122,7 @@ export default {
'common.moveSuccess': 'Move successful', 'common.moveSuccess': 'Move successful',
'common.batchMove': 'Batch move', 'common.batchMove': 'Batch move',
'common.batchCopy': 'Batch copy', 'common.batchCopy': 'Batch copy',
'common.batchCopySuccess': 'Batch copy successful',
'common.batchMoveSuccess': 'Batch move successful', 'common.batchMoveSuccess': 'Batch move successful',
'common.importSuccess': 'Import successful', 'common.importSuccess': 'Import successful',
'common.nameIsTooLang': 'The name exceeds 255 characters', 'common.nameIsTooLang': 'The name exceeds 255 characters',
@ -157,4 +158,10 @@ export default {
'common.text': 'Text', 'common.text': 'Text',
'common.resourceDeleted': 'Resource has been deleted', 'common.resourceDeleted': 'Resource has been deleted',
'common.refresh': 'Refresh', 'common.refresh': 'Refresh',
'common.searchByNameAndId': 'Search by ID, name, or tag',
'common.archive': 'archive',
'common.running': 'Running',
'common.unExecute': 'Not executed',
'common.pass': 'Pass',
'common.unPass': 'Fail pass',
}; };

View File

@ -123,6 +123,7 @@ export default {
'common.moveSuccess': '移动成功', 'common.moveSuccess': '移动成功',
'common.batchMove': '批量移动', 'common.batchMove': '批量移动',
'common.batchCopy': '批量复制', 'common.batchCopy': '批量复制',
'common.batchCopySuccess': '批量复制成功',
'common.batchMoveSuccess': '批量移动成功', 'common.batchMoveSuccess': '批量移动成功',
'common.importSuccess': '导入成功', 'common.importSuccess': '导入成功',
'common.nameIsTooLang': '名称超过255个字符', 'common.nameIsTooLang': '名称超过255个字符',
@ -157,4 +158,10 @@ export default {
'common.text': '文本', 'common.text': '文本',
'common.resourceDeleted': '资源已被删除', 'common.resourceDeleted': '资源已被删除',
'common.refresh': '刷新', 'common.refresh': '刷新',
'common.searchByNameAndId': '通过ID、名称或标签搜索',
'common.archive': '归档',
'common.running': '执行中',
'common.unExecute': '未执行',
'common.pass': '通过',
'common.unPass': '不通过',
}; };

View File

@ -1 +1,27 @@
// 计划分页
export interface TestPlanItem {
id?: string;
projectId: string;
num: number;
name: string;
status: string;
type: string;
tags: string[];
schedule: string; // 是否定时
createUser: string;
createTime: string;
moduleName: string;
moduleId: string;
children: TestPlanItem[];
childrenCount: number;
groupId: string;
}
export interface ResourcesItem {
id: string;
name: string;
cpuRate: string;
status: boolean;
}
export default {}; export default {};

View File

@ -3,31 +3,31 @@ import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { DEFAULT_LAYOUT } from '../base'; import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types'; import type { AppRouteRecordRaw } from '../types';
// const TestPlan: AppRouteRecordRaw = { const TestPlan: AppRouteRecordRaw = {
// path: '/test-plan', path: '/test-plan',
// name: TestPlanRouteEnum.TEST_PLAN, name: TestPlanRouteEnum.TEST_PLAN,
// redirect: '/test-plan/testPlanIndex', redirect: '/test-plan/testPlanIndex',
// component: DEFAULT_LAYOUT, component: DEFAULT_LAYOUT,
// meta: { meta: {
// locale: 'menu.testPlan', locale: 'menu.testPlan',
// icon: 'icon-icon_test-tracking_filled', icon: 'icon-icon_test-tracking_filled',
// order: 1, order: 1,
// hideChildrenInMenu: true, hideChildrenInMenu: true,
// roles: ['TEST_PLAN:READ'], roles: ['*'],
// }, },
// children: [ children: [
// // 测试计划 // 测试计划
// { {
// path: 'testPlanIndex', path: 'testPlanIndex',
// name: TestPlanRouteEnum.TEST_PLAN_INDEX, name: TestPlanRouteEnum.TEST_PLAN_INDEX,
// component: () => import('@/views/test-plan/testPlan/index.vue'), component: () => import('@/views/test-plan/testPlan/index.vue'),
// meta: { meta: {
// locale: 'menu.testPlan', locale: 'menu.testPlan',
// roles: ['TEST_PLAN:READ'], roles: ['*'],
// isTopMenu: true, isTopMenu: true,
// }, },
// }, },
// ], ],
// }; };
// export default TestPlan; export default TestPlan;

View File

@ -7,7 +7,12 @@
allow-clear allow-clear
:max-length="255" :max-length="255"
/> />
<a-button v-permission="['CASE_REVIEW:READ+ADD']" class="ml-2" type="primary" @click="emit('create')"> <a-button
v-if="!props.isModal && hasAnyPermission(['CASE_REVIEW:READ+ADD'])"
class="ml-2"
type="primary"
@click="emit('create')"
>
{{ t('common.newCreate') }} {{ t('common.newCreate') }}
</a-button> </a-button>
</div> </div>
@ -225,7 +230,7 @@
}; };
return { return {
...e, ...e,
hideMoreAction: e.id === 'root', hideMoreAction: e.id === 'root' || props.isModal,
draggable: e.id !== 'root' && !props.isModal, draggable: e.id !== 'root' && !props.isModal,
disabled: e.id === activeFolder.value && props.isModal, disabled: e.id === activeFolder.value && props.isModal,
count: props.modulesCount?.[e.id] || 0, // count: props.modulesCount?.[e.id] || 0, //

View File

@ -19,6 +19,7 @@
v-bind="propsRes" v-bind="propsRes"
ref="tableRef" ref="tableRef"
:action-config="tableBatchActions" :action-config="tableBatchActions"
:selectable="hasOperationPermission"
v-on="propsEvent" v-on="propsEvent"
@batch-action="handleTableBatch" @batch-action="handleTableBatch"
> >

View File

@ -22,6 +22,7 @@
v-bind="propsRes" v-bind="propsRes"
ref="tableRef" ref="tableRef"
:action-config="tableBatchActions" :action-config="tableBatchActions"
:selectable="hasOperationPermission"
v-on="propsEvent" v-on="propsEvent"
@batch-action="handleTableBatch" @batch-action="handleTableBatch"
> >

View File

@ -78,7 +78,6 @@
<a-button type="secondary" @click="cancelHandler"> <a-button type="secondary" @click="cancelHandler">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</a-button> </a-button>
<slot name="self-button"></slot>
<a-button <a-button
class="ml-3" class="ml-3"
type="primary" type="primary"

View File

@ -1,429 +0,0 @@
<template>
<ms-base-table
v-if="showType === 'list'"
v-bind="propsRes"
ref="tableRef"
class="mt-4"
:action-config="tableBatchActions"
:expanded-keys="expandedKeys"
@selected-change="handleTableSelect"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #num="{ record }">
<div v-if="(record.children || []).length > 0" class="mr-2 flex items-center" @click="expandHandler(record)">
<MsIcon
type="icon-icon_split-turn-down-left"
class="arrowIcon mr-1 text-[16px]"
:class="getIconClass(record)"
/>
<span :class="getIconClass(record)">{{ (record.children || []).length || 0 }}</span>
</div>
<span>{{ record.id }}</span>
</template>
<template #statusFilter="{ columnConfig }">
<a-trigger v-model:popup-visible="statusFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
<a-button type="text" class="arco-btn-text--secondary" @click="statusFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
<a-checkbox v-for="key of Object.keys(reviewStatusMap)" :key="key" :value="key">
<a-tag
:color="reviewStatusMap[key as ReviewStatus].color"
:class="[reviewStatusMap[key as ReviewStatus].class, 'px-[4px]']"
size="small"
>
{{ t(reviewStatusMap[key as ReviewStatus].label) }}
</a-tag>
</a-checkbox>
</a-checkbox-group>
</div>
<div class="filter-button">
<a-button size="mini" class="mr-[8px]" @click="resetStatusFilter">
{{ t('common.reset') }}
</a-button>
<a-button type="primary" size="mini" @click="handleFilterHidden(false)">
{{ t('system.orgTemplate.confirm') }}
</a-button>
</div>
</div>
</template>
</a-trigger>
</template>
<template #name="{ record }">
<a-button type="text" class="px-0">{{ record.name }}</a-button>
</template>
<template #status="{ record }">
<statusTag :status="record.status" />
</template>
<template #passRate="{ record }">
<div class="mr-[8px] w-[100px]">
<passRateLine :review-detail="record" height="5px" />
</div>
<div class="text-[var(--color-text-1)]">
{{ `${record.passRate || 0}%` }}
</div>
</template>
<template #operation="{ record }">
<MsButton>{{ t('testPlan.testPlanIndex.execution') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+ADD']">{{ t('testPlan.testPlanIndex.copy') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction
v-permission="['PROJECT_TEST_PLAN:READ+DELETE']"
:list="moreActions"
@select="handleMoreActionSelect($event, record)"
/>
</template>
</ms-base-table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/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 MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
import statusTag from '@/views/case-management/caseReview/components/statusTag.vue';
import { reviewStatusMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';
import { ReviewStatus } from '@/models/caseManagement/caseReview';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
const tableStore = useTableStore();
const { t } = useI18n();
const columns: MsTableColumn = [
{
title: 'testPlan.testPlanIndex.ID',
slotName: 'num',
dataIndex: 'num',
width: 200,
showInTable: true,
ellipsis: true,
showDrag: false,
},
{
title: 'testPlan.testPlanIndex.testPlanName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
editType: ColumnEditTypeEnum.INPUT,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
ellipsis: true,
showDrag: false,
},
{
title: 'testPlan.testPlanIndex.desc',
slotName: 'desc',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.status',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
width: 150,
},
{
title: 'testPlan.testPlanIndex.passRate',
dataIndex: 'passRate',
slotName: 'passRate',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.useCount',
slotName: 'versionId',
dataIndex: 'versionId',
width: 300,
showTooltip: true,
showInTable: true,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.bugCount',
slotName: 'moduleId',
dataIndex: 'moduleId',
showInTable: true,
width: 300,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.belongModule',
slotName: 'belongModule',
dataIndex: 'belongModule',
showInTable: true,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.creator',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.createTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 260,
showInTable: true,
showDrag: false,
},
];
/**
* 更新测试计划名称
*/
async function updatePlanName() {
try {
Message.success(t('common.updateSuccess'));
return Promise.resolve(true);
} catch (error) {
console.log(error);
return Promise.resolve(false);
}
}
const keyword = ref<string>('');
const scrollWidth = ref<number>(3400);
const statusFilterVisible = ref(false);
const statusFilters = ref<string[]>([]);
const tableBatchActions = {
baseAction: [
{
label: 'caseManagement.featureCase.export',
eventTag: 'export',
children: [
{
label: 'caseManagement.featureCase.exportExcel',
eventTag: 'exportExcel',
},
{
label: 'caseManagement.featureCase.exportXMind',
eventTag: 'exportXMind',
},
],
},
{
label: 'common.edit',
eventTag: 'batchEdit',
},
{
label: 'caseManagement.featureCase.moveTo',
eventTag: 'batchMoveTo',
},
{
label: 'caseManagement.featureCase.copyTo',
eventTag: 'batchCopyTo',
},
],
moreAction: [
{
label: 'caseManagement.featureCase.addDemand',
eventTag: 'addDemand',
},
{
label: 'caseManagement.featureCase.associatedDemand',
eventTag: 'associatedDemand',
},
{
label: 'caseManagement.featureCase.generatingDependencies',
eventTag: 'generatingDependencies',
},
{
label: 'caseManagement.featureCase.addToPublic',
eventTag: 'addToPublic',
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
},
],
};
const moreActions: ActionsItem[] = [
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
const tableSelected = ref<(string | number)[]>([]);
function handleTableSelect(selectArr: (string | number)[]) {
tableSelected.value = selectArr;
}
const data = [
{
id: '100944',
projectId: 'string',
num: '100944',
name: '系统示例',
status: 'PREPARED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
children: [
{
id: '100945',
projectId: 'string',
num: '100945',
name: '系统示例',
status: 'COMPLETED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
testPlanItem: [],
testPlanGroupId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
},
],
testPlanGroupId: 'string',
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setKeyword, setAdvanceFilter, setProps } =
useTable(
undefined,
{
tableKey: TableKeyEnum.TEST_PLAN_ALL_TABLE,
scroll: { x: scrollWidth.value },
selectable: true,
showSetting: true,
heightUsed: 374,
enableDrag: true,
},
(item) => {
return {
...item,
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
};
}
);
const showType = ref<string>('list');
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
currentSelectCount: 0,
});
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = params;
}
function deletePlan(record: any) {}
function handleMoreActionSelect(item: ActionsItem, record: any) {
if (item.eventTag === 'delete') {
deletePlan(record);
}
}
tableStore.initColumn(TableKeyEnum.TEST_PLAN_ALL_TABLE, columns, 'drawer');
const expandedKeys = ref<string[]>([]);
function expandHandler(record: any) {
if (expandedKeys.value.includes(record.id)) {
expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
} else {
expandedKeys.value = [...expandedKeys.value, record.id];
}
}
function getIconClass(record: any) {
return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
}
function searchPlan() {}
function handleFilterHidden(val: boolean) {
if (!val) {
statusFilterVisible.value = false;
searchPlan();
}
}
function resetStatusFilter() {
statusFilterVisible.value = false;
statusFilters.value = [];
searchPlan();
}
onMounted(() => {
setProps({ data });
});
</script>
<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;
}
.arrowIcon {
transform: scaleX(-1);
}
</style>

View File

@ -0,0 +1,178 @@
<template>
<a-modal
v-model:visible="showModalVisible"
title-align="start"
class="ms-modal-no-padding ms-modal-small"
:mask-closable="false"
:ok-text="props.mode === 'move' ? t('common.move') : t('common.copy')"
:ok-button-props="{ disabled: innerSelectedModuleKeys.length === 0 }"
:cancel-button-props="{ disabled: props.okLoading }"
:on-before-ok="handleCaseMoveOrCopy"
@close="handleMoveCaseModalCancel"
>
<template #title>
<div class="flex w-full items-center justify-between">
<div>
{{ props.mode === 'move' ? t('common.batchMove') : t('common.batchCopy') }}
<span class="ml-[4px] text-[var(--color-text-4)]">
{{ t('testPlan.testPlanIndex.selectedCount', { count: props.currentSelectCount }) }}
</span>
</div>
</div>
</template>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear
:max-length="255"
class="mb-4"
/>
<a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
v-model:selected-keys="innerSelectedModuleKeys"
:data="treeData"
:keyword="moduleKeyword"
:default-expand-all="props.isExpandAll"
:expand-all="isExpandAll"
:empty-text="t(props.emptyText)"
:draggable="false"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="top"
@select="nodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
</div>
</template>
</MsTree>
</a-spin>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { mapTree } from '@/utils';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common';
const appStore = useAppStore();
const { t } = useI18n();
const props = withDefaults(
defineProps<{
mode: 'move' | 'copy';
currentSelectCount: number;
visible: boolean;
isExpandAll?: boolean;
getModuleTreeApi: (params: TableQueryParams) => Promise<ModuleTreeNode[]>; //
selectedNodeKeys: (string | number)[];
okLoading: boolean;
emptyText?: string;
}>(),
{
isExpandAll: false,
emptyText: 'common.noData',
}
);
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:selectedNodeKeys', val: string[]): void;
(e: 'save'): void;
}>();
const showModalVisible = useVModel(props, 'visible', emit);
const innerSelectedModuleKeys = useVModel(props, 'selectedNodeKeys', emit);
const moduleKeyword = ref<string>('');
const focusNodeKey = ref<string>('');
//
async function handleCaseMoveOrCopy() {
emit('save');
}
function handleMoveCaseModalCancel() {
showModalVisible.value = false;
innerSelectedModuleKeys.value = [];
moduleKeyword.value = '';
}
const loading = ref<boolean>(false);
const treeData = ref<ModuleTreeNode[]>([]);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules(isSetDefaultKey = false) {
try {
loading.value = true;
const res = await props.getModuleTreeApi({ projectId: appStore.currentProjectId });
treeData.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: true,
draggable: false,
disabled: false,
};
});
if (isSetDefaultKey) {
innerSelectedModuleKeys.value = [treeData.value[0].id];
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
const virtualListProps = computed(() => {
return {
height: 'calc(60vh - 190px)',
threshold: 200,
fixedSize: true,
buffer: 15,
};
});
//
const nodeSelect = (selectedKeys: (string | number)[], node: MsTreeNodeData) => {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
innerSelectedModuleKeys.value = selectedKeys;
};
watch(
() => showModalVisible.value,
(val) => {
if (val) {
initModules();
}
}
);
</script>
<style scoped lang="less"></style>

View File

@ -1,30 +1,223 @@
<template> <template>
<MsAdvanceFilter <MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="filterConfigList" :filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields" :custom-fields-config-list="searchCustomFields"
:row-count="filterRowCount" :row-count="filterRowCount"
:search-placeholder="t('common.searchByNameAndId')"
@keyword-search="fetchData" @keyword-search="fetchData"
@adv-search="handleAdvSearch" @adv-search="fetchData"
@refresh="fetchData"
> >
<template #left> <template #left>
<div class="flex w-full justify-between">
<div class="text-[var(--color-text-1)]"
>{{ moduleNamePath }}
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span></div
>
<a-radio-group v-model="showType" type="button" class="file-show-type mr-2"> <a-radio-group v-model="showType" type="button" class="file-show-type mr-2">
<a-radio value="all" class="show-type-icon p-[2px]">{{ t('testPlan.testPlanIndex.all') }}</a-radio> <a-radio :value="testPlanTypeEnum.ALL" class="show-type-icon p-[2px]">{{
<a-radio value="testPlan" class="show-type-icon p-[2px]">{{ t('testPlan.testPlanIndex.testPlan') }}</a-radio> t('testPlan.testPlanIndex.all')
<a-radio value="testPlanGroup" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlanGroup')
}}</a-radio> }}</a-radio>
<a-radio :value="testPlanTypeEnum.TEST_PLAN" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlan')
}}</a-radio>
<!-- <a-radio value="testPlanGroup" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlanGroup')
}}</a-radio> -->
</a-radio-group> </a-radio-group>
</div>
</template> </template>
</MsAdvanceFilter> </MsAdvanceFilter>
<AllTable v-if="showType === 'all'" /> <MsBaseTable
<TestPlanTable v-if="showType === 'testPlan'" /> v-bind="propsRes"
<TestPlanGroupTable v-if="showType === 'testPlanGroup'" /> ref="tableRef"
class="mt-4"
:action-config="tableBatchActions"
:expanded-keys="expandedKeys"
filter-icon-align-left
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #num="{ record }">
<div class="flex items-center">
<div v-if="record.childrenCount" class="mr-2 flex items-center" @click="expandHandler(record)">
<MsIcon
type="icon-icon_split-turn-down-left"
class="arrowIcon mr-1 text-[16px]"
:class="getIconClass(record)"
/>
<span :class="getIconClass(record)">{{ record.childrenCount }}</span>
</div>
<div
:class="[record.childrenCount ? 'pl-0' : 'pl-[36px]']"
class="one-line-text text-[rgb(var(--primary-5))]"
>{{ record.num }}</div
>
<a-tooltip position="right" :disabled="!record.schedule" :mouse-enter-delay="300">
<MsTag v-if="record.schedule" size="small" type="link" theme="outline" class="ml-2">{{
t('testPlan.testPlanIndex.timing')
}}</MsTag>
<template #content>
<div>
<div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</div>
<div>{{ t('testPlan.testPlanIndex.nextExecutionTime') }}</div>
<!-- TODO 缺少字段 -->
<div>---</div>
</div>
<!-- TODO 缺少字段 -->
<!-- <div> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div> -->
</template>
</a-tooltip>
</div>
</template>
<template #statusFilter="{ columnConfig }">
<a-trigger v-model:popup-visible="statusFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
<a-button type="text" class="arco-btn-text--secondary" @click="statusFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
<a-checkbox v-for="key of Object.keys(reviewStatusMap)" :key="key" :value="key">
<a-tag
:color="reviewStatusMap[key as ReviewStatus].color"
:class="[reviewStatusMap[key as ReviewStatus].class, 'px-[4px]']"
size="small"
>
{{ t(reviewStatusMap[key as ReviewStatus].label) }}
</a-tag>
</a-checkbox>
</a-checkbox-group>
</div>
<div class="filter-button">
<a-button size="mini" class="mr-[8px]" @click="resetStatusFilter">
{{ t('common.reset') }}
</a-button>
<a-button type="primary" size="mini" @click="handleFilterHidden(false)">
{{ t('system.orgTemplate.confirm') }}
</a-button>
</div>
</div>
</template>
</a-trigger>
</template>
<template #status="{ record }">
<statusTag :status="record.status" />
</template>
<template #passRate="{ record }">
<div class="mr-[8px] w-[100px]">
<StatusProgress :status-detail="record.statusDetail" height="5px" />
</div>
<div class="text-[var(--color-text-1)]">
{{ `${record.passRate || 0}%` }}
</div>
</template>
<template #passRateTitleSlot="{ columnConfig }">
<div class="flex items-center text-[var(--color-text-3)]">
{{ t(columnConfig.title as string) }}
<a-tooltip position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<!-- TODO 需要提供文案 -->
<!-- <div>{{ t('apiTestDebug.encodeTip1') }}</div>
<div>{{ t('apiTestDebug.encodeTip2') }}</div> -->
</template>
</a-tooltip>
</div>
</template>
<template #useCount="{ record }">
<a-popover position="bottom" content-class="p-[16px]" trigger="click">
<div>{{ record.useCaseCount.caseCount }}</div>
<template #content>
<table class="min-w-[144px]">
<tr>
<td class="popover-label-td">
<div>{{ t('project.testPlanIndex.TotalCases') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="text-[var(--color-text-1)]">{{ t('project.testPlanIndex.functionalUseCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="text-[var(--color-text-1)]">{{ t('project.testPlanIndex.apiCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="text-[var(--color-text-1)]">{{ t('project.testPlanIndex.apiScenarioCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
</td>
</tr>
</table>
</template>
</a-popover>
</template>
<template #operation="{ record }">
<div class="flex items-center">
<MsButton class="!mx-0">{{ t('testPlan.testPlanIndex.execution') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" class="!mx-0">{{ t('common.edit') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
</div>
</template>
</MsBaseTable>
<a-modal
v-model:visible="executeVisible"
class="ms-modal-form ms-modal-small ms-modal-response-body"
unmount-on-close
title-align="start"
:mask="true"
:mask-closable="false"
@close="cancelHandler"
>
<template #title>
{{ t('testPlan.testPlanIndex.batchExecution') }}
</template>
<a-radio-group>
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
<template #footer>
<div class="flex justify-end">
<a-button type="secondary" @click="cancelHandler">
{{ t('common.cancel') }}
</a-button>
<a-button class="ml-3" type="primary" :loading="confirmLoading" @click="executeHandler">
{{ t('common.execute') }}
</a-button>
</div>
</template>
</a-modal>
<BatchMoveOrCopy
v-model:visible="showBatchModal"
v-model:selected-node-keys="selectNodeKeys"
:mode="modeType"
:current-select-count="batchParams.currentSelectCount || 0"
:get-module-tree-api="getTestPlanModule"
:ok-loading="okLoading"
@save="handleMoveOrCopy"
/>
<ScheduledModal v-model:visible="showScheduledTaskModal" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -33,13 +226,34 @@
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter'; import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type'; import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
import AllTable from './allTable.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import TestPlanGroupTable from './testplanGroup.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import TestPlanTable from './testplanTable.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 MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import BatchMoveOrCopy from './batchMoveOrCopy.vue';
import ScheduledModal from './scheduledModal.vue';
import StatusProgress from './statusProgress.vue';
import statusTag from '@/views/case-management/caseReview/components/statusTag.vue';
import { getTestPlanList, getTestPlanModule } from '@/api/modules/test-plan/testPlan';
import { reviewStatusMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore, useTableStore } from '@/store';
import { ReviewStatus } from '@/models/caseManagement/caseReview';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const tableStore = useTableStore();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal();
const props = defineProps<{ const props = defineProps<{
activeFolder: string; activeFolder: string;
activeFolderType: 'folder' | 'module'; activeFolderType: 'folder' | 'module';
@ -51,19 +265,669 @@
(e: 'init', params: any): void; (e: 'init', params: any): void;
}>(); }>();
const columns: MsTableColumn = [
{
title: 'testPlan.testPlanIndex.ID',
slotName: 'num',
dataIndex: 'num',
width: 200,
showInTable: true,
showDrag: false,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'testPlan.testPlanIndex.testPlanName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 180,
editType: ColumnEditTypeEnum.INPUT,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showDrag: false,
columnSelectorDisabled: true,
},
{
title: 'testPlan.testPlanIndex.desc',
slotName: 'desc',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.executionResult',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
showInTable: true,
showDrag: true,
width: 150,
},
{
title: 'common.creator',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.passRate',
dataIndex: 'passRate',
slotName: 'passRate',
titleSlotName: 'passRateTitleSlot',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.useCount',
slotName: 'useCount',
dataIndex: 'useCount',
showTooltip: true,
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'common.tag',
slotName: 'tags',
dataIndex: 'tags',
showInTable: true,
isTag: true,
width: 300,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.belongModule',
slotName: 'moduleName',
dataIndex: 'moduleName',
showInTable: true,
showDrag: true,
width: 200,
},
{
title: 'testPlan.testPlanIndex.createTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.planStartToEndTime',
slotName: 'planStartToEndTime',
dataIndex: 'planStartToEndTime',
showInTable: false,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.actualStartToEndTime',
slotName: 'actualStartToEndTime',
dataIndex: 'actualStartToEndTime',
showInTable: false,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 200,
showInTable: true,
showDrag: false,
},
];
/**
* 更新测试计划名称
*/
async function updatePlanName() {
try {
Message.success(t('common.updateSuccess'));
return Promise.resolve(true);
} catch (error) {
console.log(error);
return Promise.resolve(false);
}
}
const keyword = ref<string>('');
const statusFilterVisible = ref(false);
const statusFilters = ref<string[]>([]);
const tableBatchActions = {
baseAction: [
{
label: 'testPlan.testPlanIndex.execute',
eventTag: 'execute',
permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
},
{
label: 'common.copy',
eventTag: 'copy',
permission: ['PROJECT_TEST_PLAN:READ+ADD'],
},
// {
// label: 'common.export',
// eventTag: 'export',
// },
{
label: 'common.move',
eventTag: 'move',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
],
moreAction: [
{
label: 'testPlan.testPlanIndex.openTimingTask',
eventTag: 'openTimingTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'testPlan.testPlanIndex.closeTimingTask',
eventTag: 'closeTimingTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'common.archive',
eventTag: 'archive',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
permission: ['PROJECT_TEST_PLAN:READ+DELETE'],
},
],
};
const moreActions: ActionsItem[] = [
{
label: 'common.copy',
eventTag: 'copy',
},
{
label: 'testPlan.testPlanIndex.createScheduledTask',
eventTag: 'createScheduledTask',
},
{
label: 'testPlan.testPlanIndex.configuration',
eventTag: 'config',
},
{
label: 'common.archive',
eventTag: 'archive',
},
{
isDivider: true,
},
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
// TODO
const data = [
{
id: '100944',
projectId: 'string',
num: '100944',
name: '系统示例',
status: 'PREPARED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
childrenCount: 2,
statusDetail: {
tolerance: 100,
UNPENDING: 100,
RUNNING: 30,
SUCCESS: 30,
ERROR: 30,
executionProgress: '100%',
},
useCaseCount: {
caseCount: 3,
apiCount: 3,
scenarioCount: 3,
},
children: [
{
id: '100945',
projectId: 'string',
num: '100945',
name: '系统示例',
status: 'COMPLETED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
testPlanItem: [],
testPlanGroupId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
childrenCount: 0,
useCaseCount: {
caseCount: 3,
apiCount: 3,
scenarioCount: 3,
},
statusDetail: {
tolerance: 100,
UNPENDING: 100,
RUNNING: 30,
SUCCESS: 30,
ERROR: 30,
executionProgress: '100%',
},
},
{
id: '100955',
projectId: 'string',
num: '100955',
name: '系统示例',
status: 'COMPLETED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
testPlanItem: [],
testPlanGroupId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
childrenCount: 0,
useCaseCount: {
caseCount: 3,
apiCount: 3,
scenarioCount: 3,
},
statusDetail: {
tolerance: 100,
UNPENDING: 100,
RUNNING: 30,
SUCCESS: 30,
ERROR: 30,
executionProgress: '100%',
},
},
],
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setProps } = useTable(
getTestPlanList,
{
tableKey: TableKeyEnum.TEST_PLAN_ALL_TABLE,
selectable: true,
showSetting: true,
heightUsed: 128,
paginationSize: 'mini',
},
(item) => {
return {
...item,
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
};
}
);
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
currentSelectCount: 0,
});
const conditionParams = ref({
keyword: '',
filter: {},
combine: {},
});
const showType = ref<keyof typeof testPlanTypeEnum>('ALL');
async function initTableParams() {
conditionParams.value = {
keyword: keyword.value,
filter: propsRes.value.filter,
combine: batchParams.value.condition,
};
return {
type: showType.value,
moduleIds: props.activeFolder && props.activeFolder !== 'all' ? [props.activeFolder, ...props.offspringIds] : [],
projectId: appStore.currentProjectId,
excludeIds: batchParams.value.excludeIds || [],
selectAll: !!batchParams.value?.selectAll,
selectIds: batchParams.value.selectedIds || [],
keyword: keyword.value,
filter: {},
condition: {
keyword: keyword.value,
},
combine: {
...batchParams.value.condition,
},
};
}
async function fetchData() {
resetSelector();
setLoadListParams(await initTableParams());
loadList();
const tableParams = await initTableParams();
emit('init', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
/**
* 批量执行
*/
const executeVisible = ref<boolean>(false);
function handleExecute() {
executeVisible.value = true;
}
function cancelHandler() {
executeVisible.value = false;
}
const confirmLoading = ref<boolean>(false);
/**
* 执行
*/
function executeHandler() {}
/**
* 批量复制或者移动
*/
const modeType = ref<'move' | 'copy'>('move');
const showBatchModal = ref<boolean>(false);
const selectNodeKeys = ref<(string | number)[]>([]);
const okLoading = ref<boolean>(false);
function handleCopyOrMove(type: 'move' | 'copy') {
modeType.value = type;
selectNodeKeys.value = [];
showBatchModal.value = true;
}
/**
* 批量移动或复制保存
*/
async function handleMoveOrCopy() {
okLoading.value = true;
try {
const params = {
selectIds: batchParams.value.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [],
condition: {
keyword: keyword.value,
filter: {
reviewStatus: statusFilters.value,
},
combine: batchParams.value.condition,
},
projectId: appStore.currentProjectId,
moduleIds: [...selectNodeKeys.value],
};
if (modeType.value === 'copy') {
Message.success(t('common.batchCopySuccess'));
} else {
Message.success(t('common.batchMoveSuccess'));
}
} catch (error) {
console.log(error);
} finally {
okLoading.value = false;
}
}
/**
* 打开关闭定时任务
*/
function handleStatusTimingTask(status: boolean) {}
/**
* 归档
*/
function handleArchive() {
openModal({
type: 'warning',
title: t('testPlan.testPlanIndex.confirmBatchArchivePlan', {
count: batchParams.value.currentSelectCount,
}),
content: t('testPlan.testPlanIndex.confirmBatchDeletePlanContentTip'),
okText: t('common.archive'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'normal',
},
onBeforeOk: async () => {
try {
const { selectedIds, selectAll, excludeIds } = batchParams.value;
Message.success(t('common.deleteSuccess'));
fetchData();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
/**
* 归档
*/
function handleDelete() {
openModal({
type: 'error',
title: t('testPlan.testPlanIndex.confirmBatchDeletePlan', {
count: batchParams.value.currentSelectCount,
}),
content: t('testPlan.testPlanIndex.confirmBatchDeletePlanContentTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
const { selectedIds, selectAll, excludeIds } = batchParams.value;
Message.success(t('common.deleteSuccess'));
fetchData();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
/**
* 批量操作
*/
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = params;
switch (event.eventTag) {
case 'execute':
handleExecute();
break;
case 'copy':
handleCopyOrMove('copy');
break;
case 'move':
handleCopyOrMove('move');
break;
case 'openTimingTask':
handleStatusTimingTask(true);
break;
case 'closeTimingTask':
handleStatusTimingTask(false);
break;
case 'archive':
handleArchive();
break;
case 'delete':
handleDelete();
break;
default:
break;
}
}
function deletePlan(record: any) {}
function copyHandler() {}
const showScheduledTaskModal = ref<boolean>(false);
function handleScheduledTask() {
showScheduledTaskModal.value = true;
}
function handleMoreActionSelect(item: ActionsItem, record: any) {
switch (item.eventTag) {
case 'copy':
copyHandler();
break;
case 'createScheduledTask':
handleScheduledTask();
break;
default:
break;
}
}
const expandedKeys = ref<string[]>([]);
function expandHandler(record: any) {
if (expandedKeys.value.includes(record.id)) {
expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
} else {
expandedKeys.value = [...expandedKeys.value, record.id];
}
}
function getIconClass(record: any) {
return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
}
function handleFilterHidden(val: boolean) {
if (!val) {
statusFilterVisible.value = false;
fetchData();
}
}
function resetStatusFilter() {
statusFilterVisible.value = false;
statusFilters.value = [];
fetchData();
}
/** * /** *
* 高级检索 * 高级检索
*/ */
const filterConfigList = ref<FilterFormItem[]>([]); const filterConfigList = ref<FilterFormItem[]>([]);
const searchCustomFields = ref<FilterFormItem[]>([]); const searchCustomFields = ref<FilterFormItem[]>([]);
const filterRowCount = ref(0); const filterRowCount = ref(0);
const moduleNamePath = ref<string>('全部测试计划');
const showType = ref<string>('all'); watch(
() => showType.value,
(val) => {
if (val) {
fetchData();
}
}
);
function fetchData() {} watch(
() => props.activeFolder,
(val) => {
if (val) {
fetchData();
}
}
);
function handleAdvSearch() {} // TODO
// onMounted(() => {
// setProps({ data });
// });
onBeforeMount(() => {
fetchData();
});
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_ALL_TABLE, columns, 'drawer');
</script> </script>
<style scoped></style> <style scoped lang="less">
:deep(.arco-table-cell-expand-icon .arco-table-cell-inline-icon) {
display: none;
}
:deep(.arco-table-cell-align-left) > span:first-child {
padding-left: 0 !important;
}
.arrowIcon {
transform: scaleX(-1);
}
:deep(.ms-modal-form .arco-modal-body) {
padding: 0 !important;
}
.popover-label-td {
@apply flex items-center;
padding: 8px 8px 0 0;
color: var(--color-text-4);
}
.popover-value-td {
@apply font-medium;
padding-top: 8px;
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<a-modal
v-model:visible="showModalVisible"
class="ms-modal-form ms-modal-small"
title-align="start"
:mask-closable="false"
>
<template #title>
{{ form.id ? t('testPlan.testPlanIndex.updateScheduledTask') : t('testPlan.testPlanIndex.createScheduledTask') }}
</template>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item :label="t('testPlan.testPlanIndex.triggerTime')" asterisk-position="end">
<a-select v-model:model-value="form.time" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value">
<span class="text-[var(--color-text-2)]"> {{ item.value }}</span
><span class="ml-1 text-[var(--color-text-n4)] hover:text-[rgb(var(--primary-5))]">
{{ item.label }}
</span>
</a-option>
<template #footer>
<div class="mb-[6px] mt-[4px] p-[3px_8px]">
<MsButton type="text" class="text-[rgb(var(--primary-5))]" @click="createCustomFrequency">
{{ t('project.testPlanIndex.customFrequency') }}
</MsButton>
</div>
</template>
</a-select>
</a-form-item>
<a-radio-group v-model="form.env" class="mb-4">
<a-radio value="">
{{ t('project.testPlanIndex.defaultEnv') }}
<span class="float-right mx-1 mt-[1px]">
<a-tooltip :content="t('testPlan.testPlanIndex.envTip')" position="top">
<IconQuestionCircle class="h-[16px] w-[16px] text-[--color-text-4] hover:text-[rgb(var(--primary-5))]"
/></a-tooltip>
</span>
</a-radio>
<a-radio value="new"> {{ t('project.testPlanIndex.newEnv') }}</a-radio>
</a-radio-group>
<a-radio-group v-model="form.methods">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
<a-form-item :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0">
<a-select
v-model="form.resourcePoolIds"
:placeholder="t('common.pleaseSelect')"
:field-names="{ value: 'id', label: 'name' }"
>
<template #label="{ data }">
{{ data }}
</template>
<a-option v-for="item of resourcesList" :key="item.id" :value="item.id">
<div class="flex w-full items-center justify-between">
<div>
{{ item.name }}
</div>
<div class="flex items-center">
<span class="text-[var(--color-text-4)]">CPU</span>
<span class="mx-2"> {{ item.cpuRate }}</span>
<MsTag theme="outline" :type="item.status ? 'link' : 'success'" size="small">
{{ item.status ? t('project.testPlanIndex.doing') : t('project.testPlanIndex.inFreeTime') }}
</MsTag>
</div>
</div>
</a-option>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<div class="flex items-center justify-between">
<div class="flex flex-row items-center justify-center">
<a-switch v-model="form.enable" size="small" type="line" />
<span class="ml-2">{{ t('testPlan.testPlanIndex.timingState') }}</span>
<a-tooltip size="mini" position="top">
<template #content>
<div>{{ t('testPlan.testPlanIndex.timingStateEnable') }}</div>
<div>{{ t('testPlan.testPlanIndex.timingStateClose') }}</div>
</template>
<div class="mx-1 flex items-center">
<span class="mt-[2px]"
><IconQuestionCircle class="h-[16px] w-[16px] text-[--color-text-4] hover:text-[rgb(var(--primary-5))]"
/></span>
</div>
</a-tooltip>
</div>
<div>
<a-button type="secondary" class="mr-3" @click="handleCancel">{{ t('system.plugin.pluginCancel') }}</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleCreate">{{ t('common.create') }}</a-button>
</div>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { ResourcesItem } from '@/models/testPlan/testPlan';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
}>();
const showModalVisible = useVModel(props, 'visible', emit);
const initForm = {
id: '',
time: '',
env: '',
resourcePoolIds: '',
enable: false,
methods: 'parallel',
};
const form = ref({ ...initForm });
const confirmLoading = ref<boolean>(false);
const formRef = ref();
function handleCreate() {}
function resetForm() {
form.value = { ...initForm };
}
function handleCancel() {
showModalVisible.value = false;
formRef.value?.resetFields();
resetForm();
}
const syncFrequencyOptions = [
{ label: t('apiTestManagement.timeTaskHour'), value: '0 0 0/1 * * ?' },
{ label: t('apiTestManagement.timeTaskSixHour'), value: '0 0 0/6 * * ?' },
{ label: t('apiTestManagement.timeTaskTwelveHour'), value: '0 0 0/12 * * ?' },
{ label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * * ?' },
];
const resourcesList = ref<ResourcesItem[]>([
{
id: '1',
name: '200.4',
cpuRate: '80%',
status: true,
},
{
id: '2',
name: 'LOCAL',
cpuRate: '80%',
status: true,
},
]);
function createCustomFrequency() {}
</script>
<style scoped lang="less">
:deep(.arco-select-option-content) {
@apply w-full;
}
</style>

View File

@ -0,0 +1,137 @@
<template>
<MsColorLine :color-data="colorData" :height="props.height" :radius="props.radius">
<template #popoverContent>
<table class="min-w-[144px]">
<tr>
<td class="popover-label-td">
<div>{{ t('project.testPlanIndex.tolerance') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.tolerance }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div>{{ t('project.testPlanIndex.executionProgress') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.executionProgress }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-input-border)]"></div>
<div>{{ t('common.unExecute') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.UNPENDING }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--link-6))]"></div>
<div>{{ t('common.running') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.RUNNING }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div>{{ t('common.pass') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.SUCCESS }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div>{{ t('common.unPass') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.ERROR }}
</td>
</tr>
</table>
</template>
</MsColorLine>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsColorLine from '@/components/pure/ms-color-line/index.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
statusDetail: {
tolerance: number;
UNPENDING: number;
RUNNING: number;
SUCCESS: number;
ERROR: number;
executionProgress: string;
[key: string]: any;
};
height: string;
radius?: string;
}>();
const { t } = useI18n();
const getCountTotal = computed(() => {
const { UNPENDING, RUNNING, ERROR, SUCCESS } = props.statusDetail;
return UNPENDING + RUNNING + ERROR + SUCCESS;
});
const colorData = computed(() => {
if (
props.statusDetail.UNPENDING === 0 &&
props.statusDetail.RUNNING === 0 &&
props.statusDetail.ERROR === 0 &&
props.statusDetail.SUCCESS === 0
) {
return [
{
percentage: 100,
color: 'var(--color-text-n8)',
},
];
}
return [
{
percentage: (props.statusDetail.SUCCESS / getCountTotal.value) * 100,
color: 'rgb(var(--success-6))',
},
{
percentage: (props.statusDetail.ERROR / getCountTotal.value) * 100,
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.statusDetail.RUNNING / getCountTotal.value) * 100,
color: 'rgb(var(--link-6))',
},
{
percentage: (props.statusDetail.UNPENDING / getCountTotal.value) * 100,
color: 'var(--color-text-input-border)',
},
];
});
</script>
<style lang="less" scoped>
.popover-label-td {
@apply flex items-center;
padding: 8px 8px 0 0;
color: var(--color-text-4);
}
.popover-value-td {
@apply font-medium;
padding-top: 8px;
color: var(--color-text-1);
}
</style>

View File

@ -1,11 +1,5 @@
<template> <template>
<a-input-search <a-spin class="min-h-[400px] w-full" :loading="loading">
v-model:model-value="groupKeyword"
:placeholder="t('caseManagement.featureCase.searchTip')"
allow-clear
class="mb-[16px]"
></a-input-search>
<a-spin class="w-full" :style="{ height: `calc(100vh - 346px)` }" :loading="loading">
<MsTree <MsTree
v-model:focus-node-key="focusNodeKey" v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys" :selected-keys="props.selectedKeys"
@ -32,12 +26,12 @@
<template #title="nodeData"> <template #title="nodeData">
<div class="inline-flex w-full gap-[8px]"> <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="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div v-if="!props.isModal" class="ms-tree-node-count ml-[4px] text-[var(--color-text-brand)]"> <div class="ms-tree-node-count ml-[4px] text-[var(--color-text-brand)]">
{{ nodeData.count || 0 }} {{ nodeData.count || 0 }}
</div> </div>
</div> </div>
</template> </template>
<template v-if="!props.isModal" #extra="nodeData"> <template #extra="nodeData">
<MsPopConfirm <MsPopConfirm
:visible="addSubVisible" :visible="addSubVisible"
:is-delete="false" :is-delete="false"
@ -84,6 +78,7 @@
import { import {
createPlanModuleTree, createPlanModuleTree,
deletePlanModuleTree,
getTestPlanModule, getTestPlanModule,
moveTestPlanModuleTree, moveTestPlanModuleTree,
updatePlanModuleTree, updatePlanModuleTree,
@ -103,7 +98,6 @@
const loading = ref(false); const loading = ref(false);
const props = defineProps<{ const props = defineProps<{
isModal?: boolean; //
activeFolder?: string; // 使 activeFolder?: string; // 使
selectedKeys?: Array<string | number>; // key selectedKeys?: Array<string | number>; // key
isExpandAll: boolean; // isExpandAll: boolean; //
@ -111,7 +105,7 @@
modulesCount?: Record<string, number>; // modulesCount?: Record<string, number>; //
}>(); }>();
const emits = defineEmits(['update:selectedKeys', 'planTreeNodeSelect', 'init']); const emits = defineEmits(['update:selectedKeys', 'planTreeNodeSelect', 'init', 'dragUpdate']);
const currentProjectId = computed(() => appStore.currentProjectId); const currentProjectId = computed(() => appStore.currentProjectId);
@ -162,8 +156,8 @@
return { return {
...e, ...e,
hideMoreAction: e.id === 'root', hideMoreAction: e.id === 'root',
draggable: e.id !== 'root' && !props.isModal, draggable: e.id !== 'root',
disabled: e.id === props.activeFolder && props.isModal, disabled: e.id === props.activeFolder,
count: props.modulesCount?.[e.id] || 0, count: props.modulesCount?.[e.id] || 0,
}; };
}); });
@ -194,7 +188,13 @@
}, },
maskClosable: false, maskClosable: false,
onBeforeOk: async () => { onBeforeOk: async () => {
Message.success(t('caseManagement.featureCase.deleteSuccess')); try {
await deletePlanModuleTree(node.id);
Message.success(t('common.deleteSuccess'));
initModules(true);
} catch (error) {
console.log(error);
}
}, },
hideCancel: false, hideCancel: false,
}); });
@ -262,7 +262,15 @@
console.log(error); console.log(error);
} finally { } finally {
loading.value = false; loading.value = false;
initModules(); await initModules();
const treeNode = ref<MsTreeNodeData | null>(null);
treeNode.value = dropNode;
treeNode.value.children = [];
if (dropPosition === 0) {
treeNode.value.children.push(dragNode);
}
caseNodeSelect(dropNode.id, treeNode.value);
emits('dragUpdate');
} }
} }
@ -320,10 +328,10 @@
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
return { return {
height: 'calc(100vh - 366px)', height: 'calc(100vh - 240px)',
threshold: 200, threshold: 200,
fixedSize: true, fixedSize: true,
buffer: 15, // 10 padding buffer: 15,
}; };
}); });

View File

@ -1,9 +0,0 @@
<template>
<div>测试计划组 </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
</script>
<style scoped></style>

View File

@ -1,9 +0,0 @@
<template>
<div>测试计划 </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
</script>
<style scoped></style>

View File

@ -1,15 +1,27 @@
<template> <template>
<div class="rounded-2xl bg-white"> <MsCard simple no-content-padding>
<div class="p-[24px] pb-[16px]">
<a-button v-permission="['PROJECT_TEST_PLAN:READ+ADD']" type="primary">
{{ t('testPlan.testPlanIndex.createTestPlan') }}
</a-button>
</div>
<a-divider class="!my-0" />
<div class="pageWrap">
<MsSplitBox> <MsSplitBox>
<template #first> <template #first>
<div class="p-[24px] pb-0"> <div class="p-[16px] pb-0">
<div class="mb-[16px] flex justify-between">
<a-input-search
v-model:model-value="groupKeyword"
:placeholder="t('caseManagement.featureCase.searchTip')"
allow-clear
/>
<a-dropdown-button class="ml-2" type="primary" @click="handleSelect">
{{ t('common.newCreate') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption v-permission="['FUNCTIONAL_CASE:READ+IMPORT']" value="Excel">
{{ t('testPlan.testPlanIndex.newCreatePlanGroup') }}
</a-doption>
</template>
</a-dropdown-button>
</div>
<div class="test-plan h-[100%]"> <div class="test-plan h-[100%]">
<div class="case h-[38px]"> <div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')"> <div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
@ -50,7 +62,6 @@
</MsPopConfirm> </MsPopConfirm>
</div> </div>
</div> </div>
<a-divider class="my-[8px]" />
<TestPlanTree <TestPlanTree
ref="planTreeRef" ref="planTreeRef"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
@ -65,7 +76,7 @@
</div> </div>
</template> </template>
<template #second> <template #second>
<div class="p-[24px]"> <div class="p-[16px]">
<PlanTable <PlanTable
:active-folder="activeFolder" :active-folder="activeFolder"
:offspring-ids="offspringIds" :offspring-ids="offspringIds"
@ -76,8 +87,7 @@
</div> </div>
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </MsCard>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -85,6 +95,7 @@
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue'; import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
@ -107,6 +118,8 @@
const activeFolder = ref<string>('all'); const activeFolder = ref<string>('all');
const groupKeyword = ref<string>('');
// //
const getActiveClass = (type: string) => { const getActiveClass = (type: string) => {
return activeFolder.value === type ? 'folder-text case-active' : 'folder-text'; return activeFolder.value === type ? 'folder-text case-active' : 'folder-text';
@ -181,14 +194,11 @@
* 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量 * 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量
*/ */
function initModulesCount(params: any) {} function initModulesCount(params: any) {}
function handleSelect() {}
</script> </script>
<style scoped> <style scoped lang="less">
.pageWrap {
min-width: 1000px;
height: calc(100vh - 166px);
border-radius: var(--border-radius-large);
@apply bg-white;
.case { .case {
padding: 8px 4px; padding: 8px 4px;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
@ -227,5 +237,4 @@
@apply flex cursor-pointer items-center rounded-full; @apply flex cursor-pointer items-center rounded-full;
} }
} }
}
</style> </style>

View File

@ -13,12 +13,51 @@ export default {
'testPlan.testPlanIndex.testPlanName': 'name', 'testPlan.testPlanIndex.testPlanName': 'name',
'testPlan.testPlanIndex.ID': 'ID', 'testPlan.testPlanIndex.ID': 'ID',
'testPlan.testPlanIndex.desc': 'Description', 'testPlan.testPlanIndex.desc': 'Description',
'testPlan.testPlanIndex.status': 'Execution state', 'testPlan.testPlanIndex.executionResult': 'Execution Result',
'testPlan.testPlanIndex.passRate': 'Pass Rate', 'testPlan.testPlanIndex.passRate': 'Pass Rate',
'testPlan.testPlanIndex.useCount': 'Use cases', 'testPlan.testPlanIndex.useCount': 'Use cases',
'testPlan.testPlanIndex.bugCount': 'bug count', 'testPlan.testPlanIndex.bugCount': 'bug count',
'testPlan.testPlanIndex.belongModule': 'belong module', 'testPlan.testPlanIndex.belongModule': 'belong module',
'testPlan.testPlanIndex.creator': 'creator',
'testPlan.testPlanIndex.createTime': 'create time', 'testPlan.testPlanIndex.createTime': 'create time',
'testPlan.testPlanIndex.operation': 'operation', 'testPlan.testPlanIndex.operation': 'operation',
'testPlan.testPlanIndex.newCreatePlanGroup': 'New Plan group',
'testPlan.testPlanIndex.planStartToEndTime': 'plan times',
'testPlan.testPlanIndex.actualStartToEndTime': 'Actual times',
'testPlan.testPlanIndex.timing': 'Timing',
'testPlan.testPlanIndex.execute': 'execute',
'testPlan.testPlanIndex.openTimingTask': 'Open timing task',
'testPlan.testPlanIndex.closeTimingTask': 'Turn off a timed task',
'testPlan.testPlanIndex.scheduledTaskOpened': 'The scheduled task has been enabled',
'testPlan.testPlanIndex.nextExecutionTime': 'Next execution time:',
'testPlan.testPlanIndex.scheduledTaskUnEnable': 'The scheduled task is not enabled',
'testPlan.testPlanIndex.batchExecution': 'Batch execution',
'testPlan.testPlanIndex.serial': 'Serial',
'testPlan.testPlanIndex.parallel': 'Parallel',
'testPlan.testPlanIndex.confirmBatchDeletePlan': 'Are you sure to delete {count} test plans?',
'testPlan.testPlanIndex.confirmBatchDeletePlanContentTip':
'Only sub-plans within the test plan and plan group are deleted',
'testPlan.testPlanIndex.confirmBatchArchivePlan': 'Are you sure to archive {count} test plans?',
'testPlan.testPlanIndex.confirmBatchArchivePlanContent':
'Only completed test plans can be archived! \n after filing, implement information no longer update and editing, data unrecoverable, please careful operation.',
'testPlan.testPlanIndex.selectedCount': '{count} data selected',
'testPlan.testPlanIndex.createScheduledTask': 'Create Scheduled Task',
'testPlan.testPlanIndex.updateScheduledTask': 'Update Scheduled Task',
'testPlan.testPlanIndex.configuration': 'config',
'testPlan.testPlanIndex.triggerTime': 'Trigger time',
'testPlan.testPlanIndex.envTip': 'Use case save environment',
'testPlan.testPlanIndex.resourcePool': 'Resource pool',
'testPlan.testPlanIndex.timingState': 'Timing state',
'testPlan.testPlanIndex.timingStateEnable': 'Enable: Executes scheduled tasks',
'testPlan.testPlanIndex.timingStateClose': 'Close: Stops a scheduled task',
'project.testPlanIndex.customFrequency': 'Custom frequency',
'project.testPlanIndex.doing': 'Doing',
'project.testPlanIndex.inFreeTime': 'In free time',
'project.testPlanIndex.defaultEnv': 'Default Environment',
'project.testPlanIndex.newEnv': 'New Environment',
'project.testPlanIndex.executionProgress': 'Execution progress',
'project.testPlanIndex.tolerance': 'tolerance',
'project.testPlanIndex.TotalCases': 'Total use cases',
'project.testPlanIndex.functionalUseCase': 'case',
'project.testPlanIndex.apiCase': 'Api use case',
'project.testPlanIndex.apiScenarioCase': 'Api scenario use cases',
}; };

View File

@ -13,14 +13,51 @@ export default {
'testPlan.testPlanIndex.testPlanName': '测试计划名称', 'testPlan.testPlanIndex.testPlanName': '测试计划名称',
'testPlan.testPlanIndex.ID': 'ID', 'testPlan.testPlanIndex.ID': 'ID',
'testPlan.testPlanIndex.desc': '描述', 'testPlan.testPlanIndex.desc': '描述',
'testPlan.testPlanIndex.status': '执行状态', 'testPlan.testPlanIndex.executionResult': '执行结果',
'testPlan.testPlanIndex.passRate': '通过率', 'testPlan.testPlanIndex.passRate': '通过率',
'testPlan.testPlanIndex.useCount': '用例数', 'testPlan.testPlanIndex.useCount': '用例数',
'testPlan.testPlanIndex.bugCount': 'bug数', 'testPlan.testPlanIndex.bugCount': 'bug数',
'testPlan.testPlanIndex.belongModule': '所属模块', 'testPlan.testPlanIndex.belongModule': '所属模块',
'testPlan.testPlanIndex.creator': '创建人',
'testPlan.testPlanIndex.createTime': '创建时间', 'testPlan.testPlanIndex.createTime': '创建时间',
'testPlan.testPlanIndex.operation': '操作', 'testPlan.testPlanIndex.operation': '操作',
'testPlan.testPlanIndex.execution': '执行', 'testPlan.testPlanIndex.execution': '执行',
'testPlan.testPlanIndex.copy': '复制', 'testPlan.testPlanIndex.newCreatePlanGroup': '新建计划组',
'testPlan.testPlanIndex.planStartToEndTime': '计划起止时间',
'testPlan.testPlanIndex.actualStartToEndTime': '实际起止时间',
'testPlan.testPlanIndex.timing': '定时',
'testPlan.testPlanIndex.execute': '执行',
'testPlan.testPlanIndex.openTimingTask': '开启定时任务',
'testPlan.testPlanIndex.closeTimingTask': '关闭定时任务',
'testPlan.testPlanIndex.scheduledTaskOpened': '定时任务已开启',
'testPlan.testPlanIndex.nextExecutionTime': '下次执行时间:',
'testPlan.testPlanIndex.scheduledTaskUnEnable': '定时任务未开启',
'testPlan.testPlanIndex.batchExecution': '批量执行',
'testPlan.testPlanIndex.serial': '串行',
'testPlan.testPlanIndex.parallel': '并行',
'testPlan.testPlanIndex.confirmBatchDeletePlan': '确认删除 {count} 个测试计划吗?',
'testPlan.testPlanIndex.confirmBatchDeletePlanContentTip': '仅删除测试计划和计划组内的子计划',
'testPlan.testPlanIndex.confirmBatchArchivePlan': '确认归档 {count} 个测试计划吗?',
'testPlan.testPlanIndex.confirmBatchArchivePlanContent':
'仅 已完成 测试计划可归档!\n 归档后,执行信息不再更新且不可编辑,数据不可恢复,请谨慎操作!',
'testPlan.testPlanIndex.selectedCount': '(已选 {count} 项数据)',
'testPlan.testPlanIndex.createScheduledTask': '创建定时任务',
'testPlan.testPlanIndex.updateScheduledTask': '更新定时任务',
'testPlan.testPlanIndex.configuration': '配置',
'testPlan.testPlanIndex.triggerTime': '触发时间',
'testPlan.testPlanIndex.envTip': '用例保存的环境',
'testPlan.testPlanIndex.resourcePool': '资源池',
'testPlan.testPlanIndex.timingState': '定时状态',
'testPlan.testPlanIndex.timingStateEnable': '开启:执行定时任务',
'testPlan.testPlanIndex.timingStateClose': '关闭:停止定时任务',
'project.testPlanIndex.customFrequency': '自定义频率',
'project.testPlanIndex.doing': '进行中',
'project.testPlanIndex.inFreeTime': '空闲中',
'project.testPlanIndex.defaultEnv': '默认环境',
'project.testPlanIndex.newEnv': '新环境',
'project.testPlanIndex.executionProgress': '执行进度',
'project.testPlanIndex.tolerance': '容错率',
'project.testPlanIndex.TotalCases': '用例总数',
'project.testPlanIndex.functionalUseCase': '功能用例',
'project.testPlanIndex.apiCase': '接口用例',
'project.testPlanIndex.apiScenarioCase': '接口场景用例',
}; };