feat(测试计划): 测试计划组联调拖拽&执行&定时任务&任务中心数量
This commit is contained in:
parent
f1bc941411
commit
705757d8be
|
@ -12,12 +12,15 @@ import {
|
|||
batchMovePlanUrl,
|
||||
BatchRunCaseUrl,
|
||||
BatchUpdateCaseExecutorUrl,
|
||||
ConfigScheduleUrl,
|
||||
copyTestPlanUrl,
|
||||
deletePlanUrl,
|
||||
DeleteScheduleTaskUrl,
|
||||
DeleteTestPlanModuleUrl,
|
||||
DisassociateCaseUrl,
|
||||
dragPlanOnGroupUrl,
|
||||
ExecuteHistoryUrl,
|
||||
ExecutePlanUrl,
|
||||
followPlanUrl,
|
||||
GenerateReportUrl,
|
||||
GetAssociatedBugUrl,
|
||||
|
@ -59,9 +62,11 @@ import type {
|
|||
BatchExecuteFeatureCaseParams,
|
||||
BatchFeatureCaseParams,
|
||||
BatchUpdateCaseExecutorParams,
|
||||
CreateTask,
|
||||
DisassociateCaseParams,
|
||||
ExecuteHistoryItem,
|
||||
ExecuteHistoryType,
|
||||
ExecutePlan,
|
||||
FollowPlanParams,
|
||||
PassRateCountDetail,
|
||||
PlanDetailApiCaseItem,
|
||||
|
@ -281,3 +286,15 @@ export function getPlanGroupOptions(projectId: string) {
|
|||
export function dragPlanOnGroup(data: DragSortParams) {
|
||||
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}` });
|
||||
}
|
||||
|
|
|
@ -90,3 +90,9 @@ export const TestPlanAndGroupCopyUrl = '/test-plan/copy';
|
|||
export const TestPlanGroupOptionsUrl = 'test-plan/group-list';
|
||||
// 测试计划-拖拽测试计划
|
||||
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';
|
||||
|
|
|
@ -863,6 +863,12 @@
|
|||
background: var(--color-text-brand);
|
||||
line-height: 16px;
|
||||
}
|
||||
.active-badge {
|
||||
.arco-badge-text,
|
||||
.arco-badge-number {
|
||||
background-color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
.filter-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -69,12 +69,6 @@
|
|||
@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 {
|
||||
:deep(.arco-tabs-content) {
|
||||
display: none;
|
||||
|
|
|
@ -157,11 +157,11 @@
|
|||
<template v-else-if="item.showTooltip">
|
||||
<a-input
|
||||
v-if="
|
||||
editActiveKey === `${item.dataIndex}${rowIndex}` &&
|
||||
editActiveKey === `${record[rowKey || 'id']}` &&
|
||||
item.editType &&
|
||||
item.editType === ColumnEditTypeEnum.INPUT
|
||||
"
|
||||
ref="currentInputRef"
|
||||
:ref="(el: any) => setRefMap(el, `${record[rowKey|| 'id']}`)"
|
||||
v-model="record[item.dataIndex as string]"
|
||||
:max-length="255"
|
||||
@click.stop
|
||||
|
@ -375,8 +375,16 @@
|
|||
|
||||
// 编辑按钮的Active状态
|
||||
const editActiveKey = ref<string>('');
|
||||
|
||||
// 编辑项的Ref
|
||||
const currentInputRef = ref();
|
||||
|
||||
const refMap = ref<Record<string, any>>({});
|
||||
const setRefMap = (el: any, id: string) => {
|
||||
if (el) {
|
||||
refMap.value[id] = el;
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑项的初始值,用于blur时恢复旧值
|
||||
const currentEditValue = ref<string>('');
|
||||
// 是否是enter触发
|
||||
|
@ -543,7 +551,7 @@
|
|||
record[dataIndex] = currentEditValue.value;
|
||||
}
|
||||
isEnter.value = false;
|
||||
currentInputRef.value = null;
|
||||
refMap.value[record[rowKey || 'id']] = null;
|
||||
editActiveKey.value = '';
|
||||
currentEditValue.value = '';
|
||||
} else {
|
||||
|
@ -585,6 +593,16 @@
|
|||
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[]) => {
|
||||
if (!currentData || currentData.length === 1) {
|
||||
|
@ -592,36 +610,60 @@
|
|||
}
|
||||
|
||||
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 = {
|
||||
projectId: appStore.currentProjectId,
|
||||
targetId: '', // 放置目标id
|
||||
moveMode: 'BEFORE',
|
||||
moveMode: position,
|
||||
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.targetId = currentData[index + 1].raw.id;
|
||||
} else if (index > -1 && !currentData[index + 1]) {
|
||||
if (index > -1 && currentData[index - 1]) {
|
||||
params.moveMode = 'AFTER';
|
||||
params.targetId = currentData[index - 1].raw.id;
|
||||
}
|
||||
targetIndex = newIndex + 1;
|
||||
} else if (position === 'BEFORE' && newIndex < newDragData.length - 1) {
|
||||
targetIndex = newIndex + 1;
|
||||
} else {
|
||||
params.moveMode = 'AFTER';
|
||||
targetIndex = newIndex - 1;
|
||||
}
|
||||
params.targetId = newDragData[targetIndex]?.id ?? newDragData[newIndex]?.id;
|
||||
|
||||
emit('dragChange', params);
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑单元格的input
|
||||
const handleEdit = (dataIndex: string, rowIndex: number, record: TableData) => {
|
||||
editActiveKey.value = dataIndex + rowIndex;
|
||||
editActiveKey.value = record.id;
|
||||
currentEditValue.value = record[dataIndex];
|
||||
if (currentInputRef.value) {
|
||||
currentInputRef.value[0].focus();
|
||||
const refKey = `${record[rowKey as string]}`;
|
||||
if (refMap.value[refKey]) {
|
||||
refMap.value[refKey]?.focus();
|
||||
} else {
|
||||
nextTick(() => {
|
||||
currentInputRef.value[0].focus();
|
||||
refMap.value[refKey]?.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -38,6 +38,15 @@ export const defaultDetailCount: PassRateCountDetail = {
|
|||
functionalCaseCount: 0,
|
||||
apiCaseCount: 0,
|
||||
apiScenarioCount: 0,
|
||||
scheduleConfig: {
|
||||
resourceId: '',
|
||||
enable: false,
|
||||
cron: '',
|
||||
runConfig: {
|
||||
runMode: 'SERIAL',
|
||||
},
|
||||
},
|
||||
nextTriggerTime: 0,
|
||||
};
|
||||
|
||||
export const defaultExecuteForm = {
|
||||
|
|
|
@ -206,7 +206,7 @@ export interface BatchUpdateCaseExecutorParams extends BatchFeatureCaseParams {
|
|||
export interface SortFeatureCaseParams extends DragSortParams {
|
||||
testPlanId: string;
|
||||
}
|
||||
|
||||
export type RunModeType = 'SERIAL' | 'PARALLEL';
|
||||
export interface PassRateCountDetail {
|
||||
id: string;
|
||||
passThreshold: number;
|
||||
|
@ -221,6 +221,15 @@ export interface PassRateCountDetail {
|
|||
functionalCaseCount: number;
|
||||
apiCaseCount: number;
|
||||
apiScenarioCount: number;
|
||||
scheduleConfig: {
|
||||
resourceId: string;
|
||||
enable: boolean;
|
||||
cron: string;
|
||||
runConfig: {
|
||||
runMode: RunModeType;
|
||||
};
|
||||
};
|
||||
nextTriggerTime: number;
|
||||
}
|
||||
|
||||
// 执行历史
|
||||
|
@ -300,4 +309,16 @@ export interface PlanDetailExecuteHistoryItem {
|
|||
lastExecResult: LastExecuteResults;
|
||||
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 {};
|
||||
|
|
|
@ -293,10 +293,4 @@
|
|||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
:deep(.active-badge) {
|
||||
.arco-badge-text,
|
||||
.arco-badge-number {
|
||||
background-color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -374,10 +374,4 @@
|
|||
.ms-scroll-bar();
|
||||
}
|
||||
}
|
||||
:deep(.active-badge) {
|
||||
.arco-badge-text,
|
||||
.arco-badge-number {
|
||||
background-color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -788,9 +788,6 @@
|
|||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
:deep(.active .arco-badge-text) {
|
||||
background: rgb(var(--primary-5));
|
||||
}
|
||||
:deep(.tags-class .arco-form-item-label-col) {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
|
|
@ -862,7 +862,4 @@
|
|||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
:deep(.active .arco-badge-text) {
|
||||
background: rgb(var(--primary-5));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
<template>
|
||||
<div class="box">
|
||||
<div class="left" :class="getStyleClass()">
|
||||
<div class="item" :class="[activeTask === 'real' ? 'active' : '']" @click="toggleTask('real')">
|
||||
{{ t('project.taskCenter.realTimeTask') }}
|
||||
</div>
|
||||
<div class="item" :class="[activeTask === 'timing' ? 'active' : '']" @click="toggleTask('timing')">
|
||||
{{ t('project.taskCenter.scheduledTask') }}
|
||||
<div
|
||||
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>
|
||||
<a-badge
|
||||
v-if="getTextFunc(item.value) !== ''"
|
||||
:class="`${item.value === activeTask ? 'active-badge' : ''} mt-[2px]`"
|
||||
:max-count="99"
|
||||
:text="getTextFunc(item.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
@ -40,11 +50,20 @@
|
|||
import ScheduledTask from './scheduledTask.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 { TaskCenterEnum } from '@/enums/taskCenter';
|
||||
|
||||
import type { ExtractedKeys } from './utils';
|
||||
import { on } from 'events';
|
||||
|
||||
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 rightTabList = computed(() => {
|
||||
|
@ -117,40 +136,86 @@
|
|||
const listName = computed(() => {
|
||||
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>
|
||||
|
||||
<style scoped lang="less">
|
||||
.box {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.left {
|
||||
width: 252px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--color-text-n8);
|
||||
|
||||
.item {
|
||||
padding: 0 20px;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: rgb(var(--primary-5));
|
||||
background: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: calc(100% - 300px);
|
||||
flex-grow: 1; /* 自适应 */
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.no-content {
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 0;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{{ t('common.confirmDelete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="props.record?.status === 'COMPLETED'"
|
||||
v-if="showArchive"
|
||||
:loading="confirmLoading"
|
||||
class="ml-3"
|
||||
type="primary"
|
||||
|
@ -105,6 +105,10 @@
|
|||
return t('testPlan.testPlanIndex.deletePendingPlan');
|
||||
}
|
||||
});
|
||||
|
||||
const showArchive = computed(() => {
|
||||
return props.record?.status === 'COMPLETED' && props.record.groupId && props.record.groupId === 'NONE';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -108,24 +108,24 @@
|
|||
:class="`${
|
||||
record.type === testPlanTypeEnum.TEST_PLAN ? 'text-[rgb(var(--primary-5))]' : ''
|
||||
} one-line-text ${hasIndent(record)}`"
|
||||
@click="openDetail(record.id, record.type)"
|
||||
@click="openDetail(record.id)"
|
||||
>{{ record.num }}</div
|
||||
>
|
||||
<!-- TODO 待联调定时任务 -->
|
||||
<a-tooltip position="right" :disabled="record.schedule" :mouse-enter-delay="300">
|
||||
<a-tooltip position="right" :disabled="!getSchedule(record.id)" :mouse-enter-delay="300">
|
||||
<MsTag
|
||||
v-if="record.schedule"
|
||||
v-if="getSchedule(record.id)"
|
||||
size="small"
|
||||
:type="record.schedule ? 'link' : 'default'"
|
||||
:type="getScheduleEnable(record.id) ? 'link' : 'default'"
|
||||
theme="outline"
|
||||
class="ml-2"
|
||||
:tooltip-disabled="true"
|
||||
>{{ t('testPlan.testPlanIndex.timing') }}</MsTag
|
||||
>
|
||||
<template #content>
|
||||
<div v-if="record.schedule">
|
||||
<div v-if="getScheduleEnable(record.id)">
|
||||
<div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</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 v-else> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div>
|
||||
</template>
|
||||
|
@ -176,8 +176,8 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #functionalCaseCount="{ record }">
|
||||
<a-popover position="bottom" content-class="p-[16px]" :disabled="record.functionalCaseCount < 1">
|
||||
<div>{{ record.functionalCaseCount }}</div>
|
||||
<a-popover position="bottom" content-class="p-[16px]" :disabled="getFunctionalCount(record.id) < 1">
|
||||
<div>{{ getFunctionalCount(record.id) }}</div>
|
||||
<template #content>
|
||||
<table class="min-w-[140px] max-w-[176px]">
|
||||
<tr>
|
||||
|
@ -185,7 +185,7 @@
|
|||
<div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ record.caseTotal }}
|
||||
{{ defaultCountDetailMap[record.id]?.caseTotal ?? '0' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -193,7 +193,7 @@
|
|||
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.functionalUseCase') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ record.functionalCaseCount }}
|
||||
{{ getFunctionalCount(record.id) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -201,7 +201,7 @@
|
|||
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiCase') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ record.apiCaseCount }}
|
||||
{{ defaultCountDetailMap[record.id]?.apiCaseCount ?? '0' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -209,7 +209,7 @@
|
|||
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiScenarioCase') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ record.apiScenarioCount }}
|
||||
{{ defaultCountDetailMap[record.id]?.apiScenarioCount ?? '0' }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -221,17 +221,17 @@
|
|||
<div class="flex items-center">
|
||||
<MsButton
|
||||
v-if="
|
||||
record.functionalCaseCount > 0 &&
|
||||
getFunctionalCount(record.id) > 0 &&
|
||||
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) &&
|
||||
record.status !== 'ARCHIVED'
|
||||
"
|
||||
class="!mx-0"
|
||||
@click="openDetail(record.id)"
|
||||
@click="executePlan(record)"
|
||||
>{{ t('testPlan.testPlanIndex.execution') }}</MsButton
|
||||
>
|
||||
<a-divider
|
||||
v-if="
|
||||
record.functionalCaseCount > 0 &&
|
||||
getFunctionalCount(record.id) > 0 &&
|
||||
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) &&
|
||||
record.status !== 'ARCHIVED'
|
||||
"
|
||||
|
@ -254,7 +254,7 @@
|
|||
<MsButton
|
||||
v-if="
|
||||
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
|
||||
record.functionalCaseCount < 1 &&
|
||||
getFunctionalCount(record.id) < 1 &&
|
||||
record.status !== 'ARCHIVED'
|
||||
"
|
||||
class="!mx-0"
|
||||
|
@ -264,7 +264,7 @@
|
|||
<a-divider
|
||||
v-if="
|
||||
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
|
||||
record.functionalCaseCount < 1 &&
|
||||
getFunctionalCount(record.id) < 1 &&
|
||||
record.status !== 'ARCHIVED'
|
||||
"
|
||||
direction="vertical"
|
||||
|
@ -286,9 +286,9 @@
|
|||
<template #title>
|
||||
{{ t('testPlan.testPlanIndex.batchExecution') }}
|
||||
</template>
|
||||
<a-radio-group v-model="executeType">
|
||||
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
|
||||
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
|
||||
<a-radio-group v-model="executeForm.executeMode">
|
||||
<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">
|
||||
|
@ -311,8 +311,14 @@
|
|||
:type="showType"
|
||||
@save="handleMoveOrCopy"
|
||||
/>
|
||||
<!-- TODO 待联调定时任务 -->
|
||||
<ScheduledModal v-model:visible="showScheduledTaskModal" :type="currentPlanType" @close="resetPlanType" />
|
||||
<!-- TODO 待联调[编辑] 字段加到统计里边 -->
|
||||
<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()" />
|
||||
<BatchEditModal
|
||||
v-model:visible="showEditModel"
|
||||
|
@ -361,7 +367,9 @@
|
|||
batchDeletePlan,
|
||||
batchMovePlan,
|
||||
deletePlan,
|
||||
deleteScheduleTask,
|
||||
dragPlanOnGroup,
|
||||
executePlanOrGroup,
|
||||
getPlanPassRate,
|
||||
getTestPlanDetail,
|
||||
getTestPlanList,
|
||||
|
@ -379,6 +387,8 @@
|
|||
import type {
|
||||
AddTestPlanParams,
|
||||
BatchMoveParams,
|
||||
CreateTask,
|
||||
ExecutePlan,
|
||||
moduleForm,
|
||||
PassRateCountDetail,
|
||||
TestPlanItem,
|
||||
|
@ -414,7 +424,6 @@
|
|||
|
||||
const isArchived = ref<boolean>(false);
|
||||
const keyword = ref<string>('');
|
||||
const currentPlanType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
|
||||
|
||||
const hasOperationPermission = computed(() =>
|
||||
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) {
|
||||
const { status: planStatus, functionalCaseCount: useCount, schedule } = record;
|
||||
const { status: planStatus } = record;
|
||||
const useCount = defaultCountDetailMap.value[record.id]?.functionalCaseCount ?? 0;
|
||||
|
||||
// 有用例数量才可以执行 否则不展示执行
|
||||
const copyAction =
|
||||
useCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && planStatus !== 'ARCHIVED' ? copyActions : [];
|
||||
// TODO 定时任务待联调
|
||||
|
||||
let scheduledTaskAction: ActionsItem[] = [];
|
||||
if (planStatus !== 'ARCHIVED') {
|
||||
scheduledTaskAction = schedule ? updateAndDeleteScheduledActions : createScheduledActions;
|
||||
if (planStatus !== 'ARCHIVED' && record.groupId && record.groupId === 'NONE') {
|
||||
scheduledTaskAction = getSchedule(record.id) ? updateAndDeleteScheduledActions : createScheduledActions;
|
||||
}
|
||||
// 计划组下没有计划&计划组内计划不允许单独归档
|
||||
const archiveAction =
|
||||
|
@ -701,6 +723,7 @@
|
|||
(record.type === testPlanTypeEnum.TEST_PLAN && record.groupId && record.groupId !== 'NONE')
|
||||
? []
|
||||
: archiveActions;
|
||||
|
||||
// 已归档和已完成不展示归档
|
||||
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
|
||||
return [
|
||||
|
@ -741,17 +764,18 @@
|
|||
draggableCondition: true,
|
||||
});
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams } = useTable(
|
||||
getTestPlanList,
|
||||
tableProps.value,
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
|
||||
};
|
||||
},
|
||||
updatePlanName
|
||||
);
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams, setPagination } =
|
||||
useTable(
|
||||
getTestPlanList,
|
||||
tableProps.value,
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
|
||||
};
|
||||
},
|
||||
updatePlanName
|
||||
);
|
||||
|
||||
const batchParams = ref<BatchActionQueryParams>({
|
||||
selectedIds: [],
|
||||
|
@ -816,8 +840,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
|
||||
|
||||
async function getStatistics(selectedPlanIds: (string | undefined)[]) {
|
||||
try {
|
||||
const result = await getPlanPassRate(selectedPlanIds);
|
||||
|
@ -829,18 +851,8 @@
|
|||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
resetSelector();
|
||||
await loadPlanList();
|
||||
emitTableParams();
|
||||
}
|
||||
|
||||
// 测试计划详情
|
||||
function openDetail(id: string, type?: keyof typeof testPlanTypeEnum) {
|
||||
if (type && type === testPlanTypeEnum.GROUP) {
|
||||
return;
|
||||
}
|
||||
function openDetail(id: string) {
|
||||
router.push({
|
||||
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
|
||||
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);
|
||||
|
||||
function handleExecute() {
|
||||
function handleExecute(isBatch: boolean) {
|
||||
if (isBatch) {
|
||||
executeForm.value.executeIds = batchParams.value.selectedIds || [];
|
||||
}
|
||||
executeVisible.value = true;
|
||||
}
|
||||
|
||||
function cancelHandler() {
|
||||
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 {
|
||||
await executePlanOrGroup(executeForm.value);
|
||||
cancelHandler();
|
||||
Message.success(t('case.detail.execute.success'));
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
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;
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
okLoading.value = false;
|
||||
|
@ -1042,7 +1108,7 @@
|
|||
batchParams.value = params;
|
||||
switch (event.eventTag) {
|
||||
case 'execute':
|
||||
handleExecute();
|
||||
handleExecute(true);
|
||||
break;
|
||||
case 'copy':
|
||||
handleCopyOrMove('copy');
|
||||
|
@ -1072,17 +1138,19 @@
|
|||
}
|
||||
|
||||
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) {
|
||||
currentPlanType.value = record.type;
|
||||
planType.value = record.type;
|
||||
planSourceId.value = record.id;
|
||||
taskForm.value = defaultCountDetailMap.value[record.id]?.scheduleConfig;
|
||||
showScheduledTaskModal.value = true;
|
||||
}
|
||||
|
||||
function resetPlanType() {
|
||||
currentPlanType.value = testPlanTypeEnum.TEST_PLAN;
|
||||
}
|
||||
|
||||
const showStatusDeleteModal = ref<boolean>(false);
|
||||
const activeRecord = ref<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) {
|
||||
switch (item.eventTag) {
|
||||
case 'copy':
|
||||
|
@ -1166,6 +1244,12 @@
|
|||
case 'createScheduledTask':
|
||||
handleScheduledTask(record);
|
||||
break;
|
||||
case 'updateScheduledTask':
|
||||
handleScheduledTask(record);
|
||||
break;
|
||||
case 'deleteScheduledTask':
|
||||
handleDeleteScheduled(record);
|
||||
break;
|
||||
case 'delete':
|
||||
deleteStatusHandler(record);
|
||||
break;
|
||||
|
@ -1206,6 +1290,9 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
tableProps.value.draggableCondition = hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && val === 'ALL';
|
||||
setPagination({
|
||||
current: 1,
|
||||
});
|
||||
expandedKeys.value = [];
|
||||
resetFilterParams();
|
||||
fetchData();
|
||||
|
|
|
@ -6,11 +6,15 @@
|
|||
:mask-closable="false"
|
||||
>
|
||||
<template #title>
|
||||
{{ form.id ? t('testPlan.testPlanIndex.updateScheduledTask') : t('testPlan.testPlanIndex.createScheduledTask') }}
|
||||
{{
|
||||
props.taskConfig
|
||||
? 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-select v-model:model-value="form.cron" :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))]">
|
||||
|
@ -39,9 +43,9 @@
|
|||
</a-radio>
|
||||
<a-radio value="new"> {{ t('testPlan.testPlanIndex.newEnv') }}</a-radio>
|
||||
</a-radio-group> -->
|
||||
<a-radio-group v-if="props.type === testPlanTypeEnum.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 v-if="props.type === testPlanTypeEnum.GROUP" v-model="form.runConfig.runMode">
|
||||
<a-radio value="SERIAL">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
|
||||
<a-radio value="PARALLEL">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
|
||||
</a-radio-group>
|
||||
<!-- TODO 资源池暂时不做 -->
|
||||
<!-- <a-form-item :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0">
|
||||
|
@ -89,7 +93,9 @@
|
|||
</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>
|
||||
<a-button type="primary" :loading="confirmLoading" @click="handleCreate">{{
|
||||
props.taskConfig ? t('common.update') : t('common.create')
|
||||
}}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -99,54 +105,72 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { configSchedule } from '@/api/modules/test-plan/testPlan';
|
||||
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';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
taskConfig?: CreateTask;
|
||||
type: keyof typeof testPlanTypeEnum;
|
||||
sourceId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'close'): void;
|
||||
(e: 'handleSuccess'): void;
|
||||
}>();
|
||||
|
||||
const showModalVisible = useVModel(props, 'visible', emit);
|
||||
|
||||
const initForm = {
|
||||
id: '',
|
||||
time: '',
|
||||
env: '',
|
||||
resourcePoolIds: '',
|
||||
const initForm: CreateTask = {
|
||||
resourceId: '',
|
||||
cron: '',
|
||||
enable: false,
|
||||
methods: 'parallel',
|
||||
runConfig: { runMode: 'SERIAL' },
|
||||
};
|
||||
|
||||
const form = ref({ ...initForm });
|
||||
const form = ref<CreateTask>(cloneDeep(initForm));
|
||||
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
const formRef = ref();
|
||||
function handleCreate() {}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { ...initForm };
|
||||
}
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
function handleCancel() {
|
||||
showModalVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
emit('close');
|
||||
form.value = cloneDeep(initForm);
|
||||
}
|
||||
|
||||
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 = [
|
||||
|
@ -156,22 +180,14 @@
|
|||
{ 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() {}
|
||||
watch(
|
||||
() => props.taskConfig,
|
||||
(val) => {
|
||||
if (val) {
|
||||
form.value = cloneDeep(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -133,4 +133,5 @@ export default {
|
|||
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': 'Please select the Plan group',
|
||||
'testPlan.testPlanGroup.batchArchivedGroup': 'Confirm archive: {count} test plan groups',
|
||||
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': 'Are you sure to delete {count} test plan groups?',
|
||||
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': 'Delete the scheduled task successfully',
|
||||
};
|
||||
|
|
|
@ -122,4 +122,5 @@ export default {
|
|||
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': '请选择计划组',
|
||||
'testPlan.testPlanGroup.batchArchivedGroup': '确认归档:{count} 个测试计划组吗',
|
||||
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': '确认删除 {count} 个测试计划组吗?',
|
||||
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': '删除定时任务成功',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue