feat(测试计划): 测试计划组联调拖拽&执行&定时任务&任务中心数量

This commit is contained in:
xinxin.wu 2024-06-06 19:24:07 +08:00 committed by Craftsman
parent f1bc941411
commit 705757d8be
17 changed files with 414 additions and 163 deletions

View File

@ -12,12 +12,15 @@ import {
batchMovePlanUrl, batchMovePlanUrl,
BatchRunCaseUrl, BatchRunCaseUrl,
BatchUpdateCaseExecutorUrl, BatchUpdateCaseExecutorUrl,
ConfigScheduleUrl,
copyTestPlanUrl, copyTestPlanUrl,
deletePlanUrl, deletePlanUrl,
DeleteScheduleTaskUrl,
DeleteTestPlanModuleUrl, DeleteTestPlanModuleUrl,
DisassociateCaseUrl, DisassociateCaseUrl,
dragPlanOnGroupUrl, dragPlanOnGroupUrl,
ExecuteHistoryUrl, ExecuteHistoryUrl,
ExecutePlanUrl,
followPlanUrl, followPlanUrl,
GenerateReportUrl, GenerateReportUrl,
GetAssociatedBugUrl, GetAssociatedBugUrl,
@ -59,9 +62,11 @@ import type {
BatchExecuteFeatureCaseParams, BatchExecuteFeatureCaseParams,
BatchFeatureCaseParams, BatchFeatureCaseParams,
BatchUpdateCaseExecutorParams, BatchUpdateCaseExecutorParams,
CreateTask,
DisassociateCaseParams, DisassociateCaseParams,
ExecuteHistoryItem, ExecuteHistoryItem,
ExecuteHistoryType, ExecuteHistoryType,
ExecutePlan,
FollowPlanParams, FollowPlanParams,
PassRateCountDetail, PassRateCountDetail,
PlanDetailApiCaseItem, PlanDetailApiCaseItem,
@ -281,3 +286,15 @@ export function getPlanGroupOptions(projectId: string) {
export function dragPlanOnGroup(data: DragSortParams) { export function dragPlanOnGroup(data: DragSortParams) {
return MSR.post({ url: dragPlanOnGroupUrl, data }); return MSR.post({ url: dragPlanOnGroupUrl, data });
} }
// 测试计划-配置定时任务
export function configSchedule(data: CreateTask) {
return MSR.post({ url: ConfigScheduleUrl, data });
}
// 测试计划-计划&计划组-执行&批量执行
export function executePlanOrGroup(data: ExecutePlan) {
return MSR.post({ url: ExecutePlanUrl, data });
}
// 测试计划-计划&计划组-执行&批量执行
export function deleteScheduleTask(testPlanId: string) {
return MSR.get({ url: `${DeleteScheduleTaskUrl}/${testPlanId}` });
}

View File

@ -90,3 +90,9 @@ export const TestPlanAndGroupCopyUrl = '/test-plan/copy';
export const TestPlanGroupOptionsUrl = 'test-plan/group-list'; export const TestPlanGroupOptionsUrl = 'test-plan/group-list';
// 测试计划-拖拽测试计划 // 测试计划-拖拽测试计划
export const dragPlanOnGroupUrl = '/test-plan/sort'; export const dragPlanOnGroupUrl = '/test-plan/sort';
// 测试计划-创建定时任务
export const ConfigScheduleUrl = '/test-plan/schedule-config';
// 测试计划-计划&计划组-执行&批量执行
export const ExecutePlanUrl = '/test-plan-execute/start';
// 测试计划-删除定时任务
export const DeleteScheduleTaskUrl = 'test-plan/schedule-config-delete';

View File

@ -863,6 +863,12 @@
background: var(--color-text-brand); background: var(--color-text-brand);
line-height: 16px; line-height: 16px;
} }
.active-badge {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
.filter-button { .filter-button {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -69,12 +69,6 @@
@apply relative right-0 top-0 transform-none shadow-none; @apply relative right-0 top-0 transform-none shadow-none;
} }
} }
:deep(.active-badge) {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
.no-content { .no-content {
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
display: none; display: none;

View File

@ -157,11 +157,11 @@
<template v-else-if="item.showTooltip"> <template v-else-if="item.showTooltip">
<a-input <a-input
v-if=" v-if="
editActiveKey === `${item.dataIndex}${rowIndex}` && editActiveKey === `${record[rowKey || 'id']}` &&
item.editType && item.editType &&
item.editType === ColumnEditTypeEnum.INPUT item.editType === ColumnEditTypeEnum.INPUT
" "
ref="currentInputRef" :ref="(el: any) => setRefMap(el, `${record[rowKey|| 'id']}`)"
v-model="record[item.dataIndex as string]" v-model="record[item.dataIndex as string]"
:max-length="255" :max-length="255"
@click.stop @click.stop
@ -375,8 +375,16 @@
// Active // Active
const editActiveKey = ref<string>(''); const editActiveKey = ref<string>('');
// Ref // Ref
const currentInputRef = ref();
const refMap = ref<Record<string, any>>({});
const setRefMap = (el: any, id: string) => {
if (el) {
refMap.value[id] = el;
}
};
// blur // blur
const currentEditValue = ref<string>(''); const currentEditValue = ref<string>('');
// enter // enter
@ -543,7 +551,7 @@
record[dataIndex] = currentEditValue.value; record[dataIndex] = currentEditValue.value;
} }
isEnter.value = false; isEnter.value = false;
currentInputRef.value = null; refMap.value[record[rowKey || 'id']] = null;
editActiveKey.value = ''; editActiveKey.value = '';
currentEditValue.value = ''; currentEditValue.value = '';
} else { } else {
@ -585,6 +593,16 @@
emit('sorterChange', sortOrder ? { [dataIndex]: sortOrder } : {}); emit('sorterChange', sortOrder ? { [dataIndex]: sortOrder } : {});
}; };
function getCurrentList(data: TableData[], key: string, id: string) {
return data.find((item) => {
const currentChildrenIds = (item.children || []).map((e) => e[key]);
if (currentChildrenIds?.includes(id)) {
return true;
}
return false;
});
}
// //
const handleDragChange = (data: TableData[], extra: TableChangeExtra, currentData: TableData[]) => { const handleDragChange = (data: TableData[], extra: TableChangeExtra, currentData: TableData[]) => {
if (!currentData || currentData.length === 1) { if (!currentData || currentData.length === 1) {
@ -592,36 +610,60 @@
} }
if (extra && extra.dragTarget?.id) { if (extra && extra.dragTarget?.id) {
let newDragData: TableData[] = data;
let oldDragData: TableData[] = currentData;
const newDragItem = getCurrentList(data, 'id', extra.dragTarget.id);
const oldDragItem = getCurrentList(currentData, 'key', extra.dragTarget.id);
if (newDragItem && newDragItem.children && oldDragItem && oldDragItem.children) {
newDragData = newDragItem.children;
oldDragData = oldDragItem.children;
}
let oldIndex = 0;
let newIndex = 0;
newIndex = newDragData.findIndex((item: any) => item.id === extra.dragTarget?.id);
oldIndex = oldDragData.findIndex((item: any) => item.key === extra.dragTarget?.id);
let position: 'AFTER' | 'BEFORE' = 'BEFORE';
position = newIndex > oldIndex ? 'AFTER' : 'BEFORE';
const params: DragSortParams = { const params: DragSortParams = {
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
targetId: '', // id targetId: '', // id
moveMode: 'BEFORE', moveMode: position,
moveId: extra.dragTarget.id as string, // id moveId: extra.dragTarget.id as string, // id
}; };
const index = currentData.findIndex((item: any) => item.key === extra.dragTarget?.id);
if (index > -1 && currentData[index + 1]) { let targetIndex;
if (position === 'AFTER' && newIndex > 0) {
targetIndex = newIndex - 1;
} else if (position === 'AFTER') {
params.moveMode = 'BEFORE'; params.moveMode = 'BEFORE';
params.targetId = currentData[index + 1].raw.id; targetIndex = newIndex + 1;
} else if (index > -1 && !currentData[index + 1]) { } else if (position === 'BEFORE' && newIndex < newDragData.length - 1) {
if (index > -1 && currentData[index - 1]) { targetIndex = newIndex + 1;
} else {
params.moveMode = 'AFTER'; params.moveMode = 'AFTER';
params.targetId = currentData[index - 1].raw.id; targetIndex = newIndex - 1;
}
} }
params.targetId = newDragData[targetIndex]?.id ?? newDragData[newIndex]?.id;
emit('dragChange', params); emit('dragChange', params);
} }
}; };
// input // input
const handleEdit = (dataIndex: string, rowIndex: number, record: TableData) => { const handleEdit = (dataIndex: string, rowIndex: number, record: TableData) => {
editActiveKey.value = dataIndex + rowIndex; editActiveKey.value = record.id;
currentEditValue.value = record[dataIndex]; currentEditValue.value = record[dataIndex];
if (currentInputRef.value) { const refKey = `${record[rowKey as string]}`;
currentInputRef.value[0].focus(); if (refMap.value[refKey]) {
refMap.value[refKey]?.focus();
} else { } else {
nextTick(() => { nextTick(() => {
currentInputRef.value[0].focus(); refMap.value[refKey]?.focus();
}); });
} }
}; };

View File

@ -38,6 +38,15 @@ export const defaultDetailCount: PassRateCountDetail = {
functionalCaseCount: 0, functionalCaseCount: 0,
apiCaseCount: 0, apiCaseCount: 0,
apiScenarioCount: 0, apiScenarioCount: 0,
scheduleConfig: {
resourceId: '',
enable: false,
cron: '',
runConfig: {
runMode: 'SERIAL',
},
},
nextTriggerTime: 0,
}; };
export const defaultExecuteForm = { export const defaultExecuteForm = {

View File

@ -206,7 +206,7 @@ export interface BatchUpdateCaseExecutorParams extends BatchFeatureCaseParams {
export interface SortFeatureCaseParams extends DragSortParams { export interface SortFeatureCaseParams extends DragSortParams {
testPlanId: string; testPlanId: string;
} }
export type RunModeType = 'SERIAL' | 'PARALLEL';
export interface PassRateCountDetail { export interface PassRateCountDetail {
id: string; id: string;
passThreshold: number; passThreshold: number;
@ -221,6 +221,15 @@ export interface PassRateCountDetail {
functionalCaseCount: number; functionalCaseCount: number;
apiCaseCount: number; apiCaseCount: number;
apiScenarioCount: number; apiScenarioCount: number;
scheduleConfig: {
resourceId: string;
enable: boolean;
cron: string;
runConfig: {
runMode: RunModeType;
};
};
nextTriggerTime: number;
} }
// 执行历史 // 执行历史
@ -300,4 +309,16 @@ export interface PlanDetailExecuteHistoryItem {
lastExecResult: LastExecuteResults; lastExecResult: LastExecuteResults;
triggerMode: string; triggerMode: string;
} }
export interface CreateTask {
resourceId: string;
enable: boolean;
cron: string;
runConfig: { runMode: 'SERIAL' | 'PARALLEL' };
}
export interface ExecutePlan {
projectId: string;
executeIds: string[];
executeMode: RunModeType;
}
export default {}; export default {};

View File

@ -293,10 +293,4 @@
padding: 8px 16px; padding: 8px 16px;
} }
} }
:deep(.active-badge) {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
</style> </style>

View File

@ -374,10 +374,4 @@
.ms-scroll-bar(); .ms-scroll-bar();
} }
} }
:deep(.active-badge) {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
</style> </style>

View File

@ -788,9 +788,6 @@
color: rgb(var(--danger-6)); color: rgb(var(--danger-6));
} }
} }
:deep(.active .arco-badge-text) {
background: rgb(var(--primary-5));
}
:deep(.tags-class .arco-form-item-label-col) { :deep(.tags-class .arco-form-item-label-col) {
justify-content: flex-start !important; justify-content: flex-start !important;
} }

View File

@ -862,7 +862,4 @@
color: rgb(var(--danger-6)); color: rgb(var(--danger-6));
} }
} }
:deep(.active .arco-badge-text) {
background: rgb(var(--primary-5));
}
</style> </style>

View File

@ -1,11 +1,21 @@
<template> <template>
<div class="box"> <div class="box">
<div class="left" :class="getStyleClass()"> <div class="left" :class="getStyleClass()">
<div class="item" :class="[activeTask === 'real' ? 'active' : '']" @click="toggleTask('real')"> <div
{{ t('project.taskCenter.realTimeTask') }} v-for="item of menuTab"
:key="item.value"
:class="`${activeTask === item.value ? 'active' : ''} item flex items-center`"
@click="toggleTask(item.value)"
>
<div class="mr-2">
{{ item.label }}
</div> </div>
<div class="item" :class="[activeTask === 'timing' ? 'active' : '']" @click="toggleTask('timing')"> <a-badge
{{ t('project.taskCenter.scheduledTask') }} v-if="getTextFunc(item.value) !== ''"
:class="`${item.value === activeTask ? 'active-badge' : ''} mt-[2px]`"
:max-count="99"
:text="getTextFunc(item.value)"
/>
</div> </div>
</div> </div>
<div class="right"> <div class="right">
@ -40,11 +50,20 @@
import ScheduledTask from './scheduledTask.vue'; import ScheduledTask from './scheduledTask.vue';
import TestPlan from './testPlan.vue'; import TestPlan from './testPlan.vue';
import {
getOrgRealTotal,
getOrgScheduleTotal,
getProjectRealTotal,
getProjectScheduleTotal,
getSystemRealTotal,
getSystemScheduleTotal,
} from '@/api/modules/project-management/taskCenter';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { TaskCenterEnum } from '@/enums/taskCenter'; import { TaskCenterEnum } from '@/enums/taskCenter';
import type { ExtractedKeys } from './utils'; import type { ExtractedKeys } from './utils';
import { on } from 'events';
const { t } = useI18n(); const { t } = useI18n();
@ -94,7 +113,7 @@
}, },
]); ]);
const activeTask = ref(route.query.tab || 'real'); const activeTask = ref<string>((route.query.tab as string) || 'real');
const activeTab = ref<ExtractedKeys>((route.query.type as ExtractedKeys) || TaskCenterEnum.API_CASE); const activeTab = ref<ExtractedKeys>((route.query.type as ExtractedKeys) || TaskCenterEnum.API_CASE);
const rightTabList = computed(() => { const rightTabList = computed(() => {
@ -117,40 +136,86 @@
const listName = computed(() => { const listName = computed(() => {
return rightTabList.value.find((item) => item.value === activeTab.value)?.label || ''; return rightTabList.value.find((item) => item.value === activeTab.value)?.label || '';
}); });
export type menuType = 'real' | 'timing';
const menuTab: { value: menuType; label: string }[] = [
{
value: 'real',
label: t('project.taskCenter.realTimeTask'),
},
{
value: 'timing',
label: t('project.taskCenter.scheduledTask'),
},
];
const getTotalMap: Record<menuType, any> = {
real: {
system: getSystemRealTotal,
organization: getOrgRealTotal,
project: getProjectRealTotal,
},
timing: {
system: getSystemScheduleTotal,
organization: getOrgScheduleTotal,
project: getProjectScheduleTotal,
},
};
const totalMap = ref<Record<menuType, number>>({
real: 0,
timing: 0,
});
async function getTotal() {
try {
const [timingTotal, realTotal] = await Promise.all([
getTotalMap.timing[props.group](),
getTotalMap.real[props.group](),
]);
totalMap.value.timing = timingTotal;
totalMap.value.real = realTotal;
} catch (error) {
console.log(error);
}
}
function getTextFunc(activeKey: menuType) {
return totalMap.value[activeKey] > 99 ? '99+' : `${totalMap.value[activeKey]}` || '';
}
onMounted(() => {
getTotal();
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.box { .box {
display: flex; display: flex;
height: 100%; height: 100%;
.left { .left {
width: 252px; width: 252px;
height: 100%; height: 100%;
border-right: 1px solid var(--color-text-n8); border-right: 1px solid var(--color-text-n8);
.item { .item {
padding: 0 20px; padding: 0 20px;
height: 38px; height: 38px;
font-size: 14px; font-size: 14px;
line-height: 38px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
&.active { &.active {
color: rgb(var(--primary-5)); color: rgb(var(--primary-5));
background: rgb(var(--primary-1)); background: rgb(var(--primary-1));
} }
} }
} }
.right { .right {
width: calc(100% - 300px); width: calc(100% - 300px);
flex-grow: 1; /* 自适应 */ flex-grow: 1; /* 自适应 */
height: 100%; height: 100%;
} }
} }
.no-content { .no-content {
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
padding-top: 0; padding-top: 0;

View File

@ -26,7 +26,7 @@
{{ t('common.confirmDelete') }} {{ t('common.confirmDelete') }}
</a-button> </a-button>
<a-button <a-button
v-if="props.record?.status === 'COMPLETED'" v-if="showArchive"
:loading="confirmLoading" :loading="confirmLoading"
class="ml-3" class="ml-3"
type="primary" type="primary"
@ -105,6 +105,10 @@
return t('testPlan.testPlanIndex.deletePendingPlan'); return t('testPlan.testPlanIndex.deletePendingPlan');
} }
}); });
const showArchive = computed(() => {
return props.record?.status === 'COMPLETED' && props.record.groupId && props.record.groupId === 'NONE';
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -108,24 +108,24 @@
:class="`${ :class="`${
record.type === testPlanTypeEnum.TEST_PLAN ? 'text-[rgb(var(--primary-5))]' : '' record.type === testPlanTypeEnum.TEST_PLAN ? 'text-[rgb(var(--primary-5))]' : ''
} one-line-text ${hasIndent(record)}`" } one-line-text ${hasIndent(record)}`"
@click="openDetail(record.id, record.type)" @click="openDetail(record.id)"
>{{ record.num }}</div >{{ record.num }}</div
> >
<!-- TODO 待联调定时任务 --> <a-tooltip position="right" :disabled="!getSchedule(record.id)" :mouse-enter-delay="300">
<a-tooltip position="right" :disabled="record.schedule" :mouse-enter-delay="300">
<MsTag <MsTag
v-if="record.schedule" v-if="getSchedule(record.id)"
size="small" size="small"
:type="record.schedule ? 'link' : 'default'" :type="getScheduleEnable(record.id) ? 'link' : 'default'"
theme="outline" theme="outline"
class="ml-2" class="ml-2"
:tooltip-disabled="true"
>{{ t('testPlan.testPlanIndex.timing') }}</MsTag >{{ t('testPlan.testPlanIndex.timing') }}</MsTag
> >
<template #content> <template #content>
<div v-if="record.schedule"> <div v-if="getScheduleEnable(record.id)">
<div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</div> <div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</div>
<div>{{ t('testPlan.testPlanIndex.nextExecutionTime') }}</div> <div>{{ t('testPlan.testPlanIndex.nextExecutionTime') }}</div>
<div> {{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div> <div> {{ dayjs(defaultCountDetailMap[record.id]?.nextTriggerTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div> </div>
<div v-else> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div> <div v-else> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div>
</template> </template>
@ -176,8 +176,8 @@
</div> </div>
</template> </template>
<template #functionalCaseCount="{ record }"> <template #functionalCaseCount="{ record }">
<a-popover position="bottom" content-class="p-[16px]" :disabled="record.functionalCaseCount < 1"> <a-popover position="bottom" content-class="p-[16px]" :disabled="getFunctionalCount(record.id) < 1">
<div>{{ record.functionalCaseCount }}</div> <div>{{ getFunctionalCount(record.id) }}</div>
<template #content> <template #content>
<table class="min-w-[140px] max-w-[176px]"> <table class="min-w-[140px] max-w-[176px]">
<tr> <tr>
@ -185,7 +185,7 @@
<div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div> <div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ record.caseTotal }} {{ defaultCountDetailMap[record.id]?.caseTotal ?? '0' }}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -193,7 +193,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.functionalUseCase') }}</div> <div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.functionalUseCase') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ record.functionalCaseCount }} {{ getFunctionalCount(record.id) }}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -201,7 +201,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiCase') }}</div> <div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiCase') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ record.apiCaseCount }} {{ defaultCountDetailMap[record.id]?.apiCaseCount ?? '0' }}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -209,7 +209,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiScenarioCase') }}</div> <div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiScenarioCase') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ record.apiScenarioCount }} {{ defaultCountDetailMap[record.id]?.apiScenarioCount ?? '0' }}
</td> </td>
</tr> </tr>
</table> </table>
@ -221,17 +221,17 @@
<div class="flex items-center"> <div class="flex items-center">
<MsButton <MsButton
v-if=" v-if="
record.functionalCaseCount > 0 && getFunctionalCount(record.id) > 0 &&
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) &&
record.status !== 'ARCHIVED' record.status !== 'ARCHIVED'
" "
class="!mx-0" class="!mx-0"
@click="openDetail(record.id)" @click="executePlan(record)"
>{{ t('testPlan.testPlanIndex.execution') }}</MsButton >{{ t('testPlan.testPlanIndex.execution') }}</MsButton
> >
<a-divider <a-divider
v-if=" v-if="
record.functionalCaseCount > 0 && getFunctionalCount(record.id) > 0 &&
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) &&
record.status !== 'ARCHIVED' record.status !== 'ARCHIVED'
" "
@ -254,7 +254,7 @@
<MsButton <MsButton
v-if=" v-if="
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
record.functionalCaseCount < 1 && getFunctionalCount(record.id) < 1 &&
record.status !== 'ARCHIVED' record.status !== 'ARCHIVED'
" "
class="!mx-0" class="!mx-0"
@ -264,7 +264,7 @@
<a-divider <a-divider
v-if=" v-if="
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
record.functionalCaseCount < 1 && getFunctionalCount(record.id) < 1 &&
record.status !== 'ARCHIVED' record.status !== 'ARCHIVED'
" "
direction="vertical" direction="vertical"
@ -286,9 +286,9 @@
<template #title> <template #title>
{{ t('testPlan.testPlanIndex.batchExecution') }} {{ t('testPlan.testPlanIndex.batchExecution') }}
</template> </template>
<a-radio-group v-model="executeType"> <a-radio-group v-model="executeForm.executeMode">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio> <a-radio value="SERIAL">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio> <a-radio value="PARALLEL">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group> </a-radio-group>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
@ -311,8 +311,14 @@
:type="showType" :type="showType"
@save="handleMoveOrCopy" @save="handleMoveOrCopy"
/> />
<!-- TODO 待联调定时任务 --> <!-- TODO 待联调[编辑] 字段加到统计里边 -->
<ScheduledModal v-model:visible="showScheduledTaskModal" :type="currentPlanType" @close="resetPlanType" /> <ScheduledModal
v-model:visible="showScheduledTaskModal"
:type="planType"
:source-id="planSourceId"
:task-config="taskForm"
@handle-success="fetchData()"
/>
<ActionModal v-model:visible="showStatusDeleteModal" :record="activeRecord" @success="fetchData()" /> <ActionModal v-model:visible="showStatusDeleteModal" :record="activeRecord" @success="fetchData()" />
<BatchEditModal <BatchEditModal
v-model:visible="showEditModel" v-model:visible="showEditModel"
@ -361,7 +367,9 @@
batchDeletePlan, batchDeletePlan,
batchMovePlan, batchMovePlan,
deletePlan, deletePlan,
deleteScheduleTask,
dragPlanOnGroup, dragPlanOnGroup,
executePlanOrGroup,
getPlanPassRate, getPlanPassRate,
getTestPlanDetail, getTestPlanDetail,
getTestPlanList, getTestPlanList,
@ -379,6 +387,8 @@
import type { import type {
AddTestPlanParams, AddTestPlanParams,
BatchMoveParams, BatchMoveParams,
CreateTask,
ExecutePlan,
moduleForm, moduleForm,
PassRateCountDetail, PassRateCountDetail,
TestPlanItem, TestPlanItem,
@ -414,7 +424,6 @@
const isArchived = ref<boolean>(false); const isArchived = ref<boolean>(false);
const keyword = ref<string>(''); const keyword = ref<string>('');
const currentPlanType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
const hasOperationPermission = computed(() => const hasOperationPermission = computed(() =>
hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE', 'PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN:READ+ADD']) hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE', 'PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN:READ+ADD'])
@ -685,15 +694,28 @@
}, },
]; ];
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
function getFunctionalCount(id: string) {
return defaultCountDetailMap.value[id]?.functionalCaseCount ?? 0;
}
function getSchedule(id: string) {
return !!defaultCountDetailMap.value[id]?.scheduleConfig;
}
function getScheduleEnable(id: string) {
return defaultCountDetailMap.value[id].scheduleConfig.enable;
}
function getMoreActions(record: TestPlanItem) { function getMoreActions(record: TestPlanItem) {
const { status: planStatus, functionalCaseCount: useCount, schedule } = record; const { status: planStatus } = record;
const useCount = defaultCountDetailMap.value[record.id]?.functionalCaseCount ?? 0;
// //
const copyAction = const copyAction =
useCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && planStatus !== 'ARCHIVED' ? copyActions : []; useCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && planStatus !== 'ARCHIVED' ? copyActions : [];
// TODO
let scheduledTaskAction: ActionsItem[] = []; let scheduledTaskAction: ActionsItem[] = [];
if (planStatus !== 'ARCHIVED') { if (planStatus !== 'ARCHIVED' && record.groupId && record.groupId === 'NONE') {
scheduledTaskAction = schedule ? updateAndDeleteScheduledActions : createScheduledActions; scheduledTaskAction = getSchedule(record.id) ? updateAndDeleteScheduledActions : createScheduledActions;
} }
// & // &
const archiveAction = const archiveAction =
@ -701,6 +723,7 @@
(record.type === testPlanTypeEnum.TEST_PLAN && record.groupId && record.groupId !== 'NONE') (record.type === testPlanTypeEnum.TEST_PLAN && record.groupId && record.groupId !== 'NONE')
? [] ? []
: archiveActions; : archiveActions;
// //
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') { if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
return [ return [
@ -741,7 +764,8 @@
draggableCondition: true, draggableCondition: true,
}); });
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams, setPagination } =
useTable(
getTestPlanList, getTestPlanList,
tableProps.value, tableProps.value,
(item) => { (item) => {
@ -816,8 +840,6 @@
}); });
} }
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
async function getStatistics(selectedPlanIds: (string | undefined)[]) { async function getStatistics(selectedPlanIds: (string | undefined)[]) {
try { try {
const result = await getPlanPassRate(selectedPlanIds); const result = await getPlanPassRate(selectedPlanIds);
@ -829,18 +851,8 @@
console.log(error); console.log(error);
} }
} }
async function fetchData() {
resetSelector();
await loadPlanList();
emitTableParams();
}
// //
function openDetail(id: string, type?: keyof typeof testPlanTypeEnum) { function openDetail(id: string) {
if (type && type === testPlanTypeEnum.GROUP) {
return;
}
router.push({ router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL, name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
query: { query: {
@ -849,29 +861,82 @@
}); });
} }
async function fetchData() {
resetSelector();
await loadPlanList();
emitTableParams();
}
/** /**
* 批量执行 * 批量执行
*/ */
const executeType = ref('serial'); const initExecuteForm: ExecutePlan = {
projectId: appStore.currentProjectId,
executeIds: [],
executeMode: 'SERIAL',
};
const executeForm = ref<ExecutePlan>(cloneDeep(initExecuteForm));
const executeVisible = ref<boolean>(false); const executeVisible = ref<boolean>(false);
function handleExecute() { function handleExecute(isBatch: boolean) {
if (isBatch) {
executeForm.value.executeIds = batchParams.value.selectedIds || [];
}
executeVisible.value = true; executeVisible.value = true;
} }
function cancelHandler() { function cancelHandler() {
executeVisible.value = false; executeVisible.value = false;
executeForm.value = cloneDeep(initExecuteForm);
} }
const confirmLoading = ref<boolean>(false);
/** /**
* 执行 TODO 待联调 * 批量执行
*/ */
function executeHandler() {
const confirmLoading = ref<boolean>(false);
async function executeHandler() {
confirmLoading.value = true;
try { try {
await executePlanOrGroup(executeForm.value);
cancelHandler();
Message.success(t('case.detail.execute.success')); Message.success(t('case.detail.execute.success'));
fetchData();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
confirmLoading.value = false;
}
}
//
function executePlan(record: TestPlanItem) {
const { type, id } = record;
if (type === testPlanTypeEnum.GROUP) {
handleExecute(false);
executeForm.value.executeIds = [id];
return;
}
if (type === testPlanTypeEnum.TEST_PLAN) {
//
if (defaultCountDetailMap.value[id]) {
const { apiScenarioCount, apiCaseCount } = defaultCountDetailMap.value[id];
if (!apiScenarioCount && !apiCaseCount) {
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
query: {
id,
},
});
} else {
executeForm.value.executeIds = [id];
executeHandler();
}
}
} }
} }
@ -920,6 +985,7 @@
showBatchModal.value = false; showBatchModal.value = false;
fetchData(); fetchData();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally { } finally {
okLoading.value = false; okLoading.value = false;
@ -1042,7 +1108,7 @@
batchParams.value = params; batchParams.value = params;
switch (event.eventTag) { switch (event.eventTag) {
case 'execute': case 'execute':
handleExecute(); handleExecute(true);
break; break;
case 'copy': case 'copy':
handleCopyOrMove('copy'); handleCopyOrMove('copy');
@ -1072,17 +1138,19 @@
} }
const showScheduledTaskModal = ref<boolean>(false); const showScheduledTaskModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem>();
const taskForm = ref<CreateTask>();
const planSourceId = ref<string>();
const planType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
function handleScheduledTask(record: TestPlanItem) { function handleScheduledTask(record: TestPlanItem) {
currentPlanType.value = record.type; planType.value = record.type;
planSourceId.value = record.id;
taskForm.value = defaultCountDetailMap.value[record.id]?.scheduleConfig;
showScheduledTaskModal.value = true; showScheduledTaskModal.value = true;
} }
function resetPlanType() {
currentPlanType.value = testPlanTypeEnum.TEST_PLAN;
}
const showStatusDeleteModal = ref<boolean>(false); const showStatusDeleteModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem>();
// : // :
async function handleDeleteGroup(record: TestPlanItem) { async function handleDeleteGroup(record: TestPlanItem) {
@ -1158,6 +1226,16 @@
} }
} }
async function handleDeleteScheduled(record: TestPlanItem) {
try {
await deleteScheduleTask(record.id);
Message.success(t('testPlan.testPlanGroup.deleteScheduleTaskSuccess'));
fetchData();
} catch (error) {
console.log(error);
}
}
function handleMoreActionSelect(item: ActionsItem, record: TestPlanItem) { function handleMoreActionSelect(item: ActionsItem, record: TestPlanItem) {
switch (item.eventTag) { switch (item.eventTag) {
case 'copy': case 'copy':
@ -1166,6 +1244,12 @@
case 'createScheduledTask': case 'createScheduledTask':
handleScheduledTask(record); handleScheduledTask(record);
break; break;
case 'updateScheduledTask':
handleScheduledTask(record);
break;
case 'deleteScheduledTask':
handleDeleteScheduled(record);
break;
case 'delete': case 'delete':
deleteStatusHandler(record); deleteStatusHandler(record);
break; break;
@ -1206,6 +1290,9 @@
(val) => { (val) => {
if (val) { if (val) {
tableProps.value.draggableCondition = hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && val === 'ALL'; tableProps.value.draggableCondition = hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && val === 'ALL';
setPagination({
current: 1,
});
expandedKeys.value = []; expandedKeys.value = [];
resetFilterParams(); resetFilterParams();
fetchData(); fetchData();

View File

@ -6,11 +6,15 @@
:mask-closable="false" :mask-closable="false"
> >
<template #title> <template #title>
{{ form.id ? t('testPlan.testPlanIndex.updateScheduledTask') : t('testPlan.testPlanIndex.createScheduledTask') }} {{
props.taskConfig
? t('testPlan.testPlanIndex.updateScheduledTask')
: t('testPlan.testPlanIndex.createScheduledTask')
}}
</template> </template>
<a-form ref="formRef" :model="form" layout="vertical"> <a-form ref="formRef" :model="form" layout="vertical">
<a-form-item :label="t('testPlan.testPlanIndex.triggerTime')" asterisk-position="end"> <a-form-item :label="t('testPlan.testPlanIndex.triggerTime')" asterisk-position="end">
<a-select v-model:model-value="form.time" :placeholder="t('common.pleaseSelect')"> <a-select v-model:model-value="form.cron" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value"> <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="text-[var(--color-text-2)]"> {{ item.value }}</span
><span class="ml-1 text-[var(--color-text-n4)] hover:text-[rgb(var(--primary-5))]"> ><span class="ml-1 text-[var(--color-text-n4)] hover:text-[rgb(var(--primary-5))]">
@ -39,9 +43,9 @@
</a-radio> </a-radio>
<a-radio value="new"> {{ t('testPlan.testPlanIndex.newEnv') }}</a-radio> <a-radio value="new"> {{ t('testPlan.testPlanIndex.newEnv') }}</a-radio>
</a-radio-group> --> </a-radio-group> -->
<a-radio-group v-if="props.type === testPlanTypeEnum.GROUP" v-model="form.methods"> <a-radio-group v-if="props.type === testPlanTypeEnum.GROUP" v-model="form.runConfig.runMode">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio> <a-radio value="SERIAL">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio> <a-radio value="PARALLEL">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group> </a-radio-group>
<!-- TODO 资源池暂时不做 --> <!-- TODO 资源池暂时不做 -->
<!-- <a-form-item :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0"> <!-- <a-form-item :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0">
@ -89,7 +93,9 @@
</div> </div>
<div> <div>
<a-button type="secondary" class="mr-3" @click="handleCancel">{{ t('system.plugin.pluginCancel') }}</a-button> <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> <a-button type="primary" :loading="confirmLoading" @click="handleCreate">{{
props.taskConfig ? t('common.update') : t('common.create')
}}</a-button>
</div> </div>
</div> </div>
</template> </template>
@ -99,54 +105,72 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { type FormInstance, Message, type ValidatedError } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import { configSchedule } from '@/api/modules/test-plan/testPlan';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { ResourcesItem } from '@/models/testPlan/testPlan'; import type { CreateTask } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum'; import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
taskConfig?: CreateTask;
type: keyof typeof testPlanTypeEnum; type: keyof typeof testPlanTypeEnum;
sourceId?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', val: boolean): void; (e: 'update:visible', val: boolean): void;
(e: 'close'): void; (e: 'close'): void;
(e: 'handleSuccess'): void;
}>(); }>();
const showModalVisible = useVModel(props, 'visible', emit); const showModalVisible = useVModel(props, 'visible', emit);
const initForm = { const initForm: CreateTask = {
id: '', resourceId: '',
time: '', cron: '',
env: '',
resourcePoolIds: '',
enable: false, enable: false,
methods: 'parallel', runConfig: { runMode: 'SERIAL' },
}; };
const form = ref({ ...initForm }); const form = ref<CreateTask>(cloneDeep(initForm));
const confirmLoading = ref<boolean>(false); const confirmLoading = ref<boolean>(false);
const formRef = ref(); const formRef = ref<FormInstance | null>(null);
function handleCreate() {}
function resetForm() {
form.value = { ...initForm };
}
function handleCancel() { function handleCancel() {
showModalVisible.value = false; showModalVisible.value = false;
formRef.value?.resetFields(); formRef.value?.resetFields();
resetForm(); form.value = cloneDeep(initForm);
emit('close'); }
function handleCreate() {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
confirmLoading.value = true;
try {
if (props.sourceId) {
const params = {
...form.value,
resourceId: props.sourceId,
};
await configSchedule(params);
handleCancel();
emit('handleSuccess');
Message.success(t('common.createSuccess'));
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
}
});
} }
const syncFrequencyOptions = [ const syncFrequencyOptions = [
@ -156,22 +180,14 @@
{ label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * * ?' }, { label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * * ?' },
]; ];
const resourcesList = ref<ResourcesItem[]>([ watch(
{ () => props.taskConfig,
id: '1', (val) => {
name: '200.4', if (val) {
cpuRate: '80%', form.value = cloneDeep(val);
status: true, }
}, }
{ );
id: '2',
name: 'LOCAL',
cpuRate: '80%',
status: true,
},
]);
function createCustomFrequency() {}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -133,4 +133,5 @@ export default {
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': 'Please select the Plan group', 'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': 'Please select the Plan group',
'testPlan.testPlanGroup.batchArchivedGroup': 'Confirm archive: {count} test plan groups', 'testPlan.testPlanGroup.batchArchivedGroup': 'Confirm archive: {count} test plan groups',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': 'Are you sure to delete {count} test plan groups?', 'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': 'Are you sure to delete {count} test plan groups?',
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': 'Delete the scheduled task successfully',
}; };

View File

@ -122,4 +122,5 @@ export default {
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': '请选择计划组', 'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': '请选择计划组',
'testPlan.testPlanGroup.batchArchivedGroup': '确认归档:{count} 个测试计划组吗', 'testPlan.testPlanGroup.batchArchivedGroup': '确认归档:{count} 个测试计划组吗',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': '确认删除 {count} 个测试计划组吗?', 'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': '确认删除 {count} 个测试计划组吗?',
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': '删除定时任务成功',
}; };