feat(接口管理): 新增接口用例回收站页面

This commit is contained in:
guoyuqi 2024-03-13 15:24:47 +08:00 committed by 刘瑞斌
parent c5602b2033
commit 045092b13b
6 changed files with 643 additions and 3 deletions

View File

@ -7,9 +7,11 @@ import {
BatchCleanOutApiUrl,
BatchDeleteCaseUrl,
BatchDeleteDefinitionUrl,
BatchDeleteRecycleCaseUrl,
BatchEditCaseUrl,
BatchMoveDefinitionUrl,
BatchRecoverApiUrl,
BatchRecoverCaseUrl,
BatchUpdateDefinitionUrl,
CasePageUrl,
CheckDefinitionScheduleUrl,
@ -23,6 +25,7 @@ import {
DeleteMockUrl,
DeleteModuleUrl,
DeleteRecycleApiUrl,
DeleteRecycleCaseUrl,
GetDefinitionDetailUrl,
GetDefinitionScheduleUrl,
GetEnvModuleUrl,
@ -34,8 +37,10 @@ import {
ImportDefinitionUrl,
MoveModuleUrl,
OperationHistoryUrl,
RecoverCaseUrl,
RecoverDefinitionUrl,
RecoverOperationHistoryUrl,
RecycleCasePageUrl,
SaveOperationHistoryUrl,
SortCaseUrl,
SortDefinitionUrl,
@ -345,6 +350,34 @@ export function dragSort(data: DragSortParams) {
return MSR.post({ url: SortCaseUrl, data });
}
/**
*
*/
// 获取回收站接口用例列表
export function getRecycleCasePage(data: ApiCasePageParams) {
return MSR.post<CommonList<ApiCaseDetail>>({ url: RecycleCasePageUrl, data });
}
// 恢复接口用例
export function recoverCase(id: string) {
return MSR.get({ url: RecoverCaseUrl, params: id });
}
// 批量恢复接口用例
export function batchRecoverCase(data: ApiCaseBatchParams) {
return MSR.post({ url: BatchRecoverCaseUrl, data });
}
// 彻底删除接口用例
export function deleteRecycleCase(id: string) {
return MSR.get({ url: DeleteRecycleCaseUrl, params: id });
}
// 批量彻底删除接口用例
export function batchDeleteRecycleCase(data: ApiCaseBatchParams) {
return MSR.post({ url: BatchDeleteRecycleCaseUrl, data });
}
// 添加接口用例
export function addCase(data: AddApiCaseParams) {
return MSR.post({ url: AddCaseUrl, data });

View File

@ -57,8 +57,17 @@ export const GetTrashModuleCountUrl = '/api/definition/module/trash/count'; //
export const CasePageUrl = '/api/case/page'; // 接口用例列表
export const UpdateCaseStatusUrl = '/api/case/update-status'; // 接口用例更新状态
export const UpdateCasePriorityUrl = '/api/case/update-priority'; // 接口用例更新等级
export const DeleteCaseUrl = '/api/case/delete'; // 删除接口用例
export const BatchDeleteCaseUrl = '/api/case/batch/delete'; // 批量删除接口用例
export const DeleteCaseUrl = '/api/case/delete-to-gc'; // 删除接口用例
export const BatchDeleteCaseUrl = '/api/case/batch/delete-to-gc'; // 批量删除接口用例
export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例
export const SortCaseUrl = '/api/case/edit/pos'; // 接口用例拖拽
/**
*
*/
export const RecycleCasePageUrl = '/api/case/trash/page'; // 接口用例回收站列表
export const RecoverCaseUrl = '/api/case/recover'; // 接口用例恢复
export const BatchRecoverCaseUrl = '/api/case/batch/recover'; // 接口用例批量恢复
export const DeleteRecycleCaseUrl = '/api/case/delete'; // 接口用例彻底删除
export const BatchDeleteRecycleCaseUrl = '/api/case/batch/delete'; // 接口用例批量彻底删除
export const AddCaseUrl = '/api/case/add'; // 添加用例

View File

@ -0,0 +1,582 @@
<template>
<div class="p-[16px_22px]">
<div class="mb-[16px] flex items-center justify-end">
<div class="flex items-center gap-[8px]">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiTestManagement.searchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadCaseList"
@press-enter="loadCaseList"
/>
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]" @click="loadCaseList">
<template #icon>
<icon-refresh class="text-[var(--color-text-4)]" />
</template>
</a-button>
</div>
</div>
<ms-base-table
v-bind="propsRes"
:action-config="batchActions"
:first-column-width="44"
no-disable
filter-icon-align-left
v-on="propsEvent"
@selected-change="handleTableSelect"
@batch-action="handleTableBatch"
>
<template #caseLevelFilter="{ columnConfig }">
<a-trigger v-model:popup-visible="caseFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
<MsButton type="text" class="arco-btn-text--secondary ml-[10px]" @click="caseFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="caseFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</MsButton>
<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="caseFilters" direction="vertical" size="small">
<a-checkbox v-for="item of caseLevelList" :key="item.text" :value="item.text">
<caseLevel :case-level="item.text" />
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #deleteTime="{ record }">
{{ dayjs(record.deleteTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
</template>
<template #status="{ record }">
<apiStatus :status="record.status" />
</template>
<template #statusFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="statusFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<MsButton type="text" class="arco-btn-text--secondary ml-[10px]" @click="statusFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</MsButton>
<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="val of Object.values(RequestDefinitionStatus)" :key="val" :value="val">
<apiStatus :status="val" />
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #lastReportStatusFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="lastReportStatusFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<MsButton
type="text"
class="arco-btn-text--secondary ml-[10px]"
@click="lastReportStatusFilterVisible = true"
>
{{ t(columnConfig.title as string) }}
<icon-down :class="lastReportStatusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</MsButton>
<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="lastReportStatusFilters" direction="vertical" size="small">
<a-checkbox v-for="val of lastReportStatusList" :key="val" :value="val">
<span>{{ val }}</span>
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #passRateColumn>
<div class="flex items-center text-[var(--color-text-3)]">
{{ t('case.passRate') }}
<a-tooltip :content="t('case.passRateTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</template>
<template #action="{ record }">
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
type="text"
class="!mr-0"
@click="recover(record)"
>
{{ t('apiTestManagement.recycle.batchRecover') }}
</MsButton>
<a-divider
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
direction="vertical"
:margin="8"
></a-divider>
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
type="text"
class="!mr-0"
@click="cleanOut(record)"
>
{{ t('apiTestManagement.recycle.batchCleanOut') }}
</MsButton>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import {
batchDeleteRecycleCase,
batchRecoverCase,
deleteRecycleCase,
getRecycleCasePage,
recoverCase,
} from '@/api/modules/api-test/management';
import { getCaseDefaultFields } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app';
import { ApiCaseBatchParams, ApiCaseDetail } from '@/models/apiTest/management';
import { RequestDefinitionStatus } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
const props = defineProps<{
activeModule: string;
offspringIds: string[];
protocol: string; //
}>();
const appStore = useAppStore();
const { t } = useI18n();
const tableStore = useTableStore();
const { openModal } = useModal();
const keyword = ref('');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 100,
},
{
title: 'case.caseName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
{
title: 'case.caseLevel',
dataIndex: 'priority',
slotName: 'caseLevel',
titleSlotName: 'caseLevelFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
{
title: 'apiTestManagement.apiStatus',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
{
title: 'apiTestManagement.path',
dataIndex: 'path',
showTooltip: true,
width: 150,
},
{
title: 'common.tag',
dataIndex: 'tags',
isTag: true,
isStringTag: true,
width: 150,
},
{
title: 'case.lastReportStatus',
dataIndex: 'lastReportStatus',
titleSlotName: 'lastReportStatusFilter',
showInTable: false,
showTooltip: true,
width: 150,
},
{
title: 'case.passRate',
dataIndex: 'passRate',
titleSlotName: 'passRateColumn',
showInTable: false,
showTooltip: true,
width: 150,
},
{
title: 'case.caseEnvironment',
dataIndex: 'environmentName',
showTooltip: true,
showInTable: false,
width: 150,
},
{
title: 'case.tableColumnUpdateUser',
dataIndex: 'updateUser',
showInTable: true,
showTooltip: true,
width: 180,
},
{
title: 'case.tableColumnUpdateTime',
dataIndex: 'updateTime',
showInTable: true,
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
{
title: 'case.tableColumnCreateUser',
dataIndex: 'createName',
showTooltip: true,
width: 180,
},
{
title: 'case.tableColumnCreateTime',
dataIndex: 'createTime',
showTooltip: true,
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
{
title: 'apiTestManagement.deleteUser',
dataIndex: 'deleteName',
showInTable: true,
showTooltip: true,
width: 180,
},
{
title: 'apiTestManagement.deleteTime',
slotName: 'deleteTime',
dataIndex: 'deleteTime',
showInTable: true,
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
{
title: 'common.operation',
slotName: 'action',
dataIndex: 'operation',
fixed: 'right',
width: 150,
},
];
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_CASE, columns, 'drawer');
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getRecycleCasePage, {
columns,
scroll: { x: '100%' },
tableKey: TableKeyEnum.API_TEST_MANAGEMENT_CASE,
showSetting: true,
selectable: true,
showSelectAll: true,
draggable: { type: 'handle', width: 32 },
});
const batchActions = {
baseAction: [
{
label: 'apiTestManagement.recycle.batchRecover',
eventTag: 'batchRecover',
permission: ['PROJECT_API_DEFINITION_CASE:READ+DELETE'],
},
{
label: 'apiTestManagement.recycle.batchCleanOut',
eventTag: 'batchCleanOut',
danger: true,
permission: ['PROJECT_API_DEFINITION_CASE:READ+DELETE'],
},
],
};
const statusFilterVisible = ref(false);
const statusFilters = ref(Object.keys(RequestDefinitionStatus));
const caseLevelFields = ref<Record<string, any>>({});
const caseFilterVisible = ref(false);
const caseFilters = ref<string[]>([]);
const caseLevelList = computed(() => {
return caseLevelFields.value?.options || [];
});
const lastReportStatusFilterVisible = ref(false);
const lastReportStatusList = ['error', 'FakeError', 'success'];
const lastReportStatusFilters = ref<string[]>([...lastReportStatusList]);
const moduleIds = computed(() => {
return props.activeModule === 'all' ? [] : [props.activeModule];
});
function loadCaseList() {
const params = {
keyword: keyword.value,
projectId: appStore.currentProjectId,
moduleIds: moduleIds.value,
protocol: props.protocol,
filter: {
status: statusFilters.value,
priority: caseFilters.value,
lastReportStatus: lastReportStatusFilters.value,
},
};
setLoadListParams(params);
loadList();
}
function loadCaseListAndResetSelector() {
resetSelector();
loadCaseList();
}
//
async function getCaseLevelFields() {
const result = await getCaseDefaultFields(appStore.currentProjectId);
caseLevelFields.value = result.customFields.find((item: any) => item.internal && item.fieldName === '用例等级');
caseFilters.value = caseLevelFields.value?.options.map((item: any) => item.text);
}
onBeforeMount(() => {
getCaseLevelFields();
loadCaseList();
});
function handleFilterHidden(val: boolean) {
if (!val) {
loadCaseList();
}
}
watch(
() => props.activeModule,
() => {
loadCaseListAndResetSelector();
}
);
watch(
() => props.protocol,
() => {
loadCaseListAndResetSelector();
}
);
const tableSelected = ref<(string | number)[]>([]); //
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
currentSelectCount: 0,
});
//
async function cleanOut(record: ApiCaseDetail) {
openModal({
type: 'error',
title: t('case.batchDeleteCaseTipTitle', { name: record?.name }),
content: t('case.recycle.cleanOutDeleteOnRecycleTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await deleteRecycleCase(record.id);
Message.success(t('common.deleteSuccess'));
loadCaseListAndResetSelector();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
async function recover(record: ApiCaseDetail) {
openModal({
type: 'info',
title: t('case.batchRecoverCaseTipTitle', { name: record?.name }),
content: t('case.recycle.recoverCaseTip'),
okText: t('case.recycle.confirmRecovery'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'normal',
},
onBeforeOk: async () => {
try {
await recoverCase(record.id);
Message.success(t('apiTestManagement.recycle.recoveredSuccessfully'));
loadCaseListAndResetSelector();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
//
function getBatchParams(): ApiCaseBatchParams {
return {
excludeIds: batchParams.value.excludeIds,
selectAll: batchParams.value.selectAll,
selectIds: batchParams.value.selectedIds as string[],
moduleIds: moduleIds.value,
projectId: appStore.currentProjectId,
protocol: props.protocol,
condition: {
keyword: keyword.value,
filter: propsRes.value.filter,
combine: batchParams.value.condition,
},
};
}
//
async function batchRecover() {
openModal({
type: 'info',
title: t('case.batchRecoverCaseTip', {
count: batchParams.value.currentSelectCount || tableSelected.value.length,
}),
content: t('case.recycle.recoverCaseTip'),
okText: t('case.recycle.confirmRecovery'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'normal',
},
onBeforeOk: async () => {
try {
await batchRecoverCase(getBatchParams());
Message.success(t('apiTestManagement.recycle.recoveredSuccessfully'));
loadCaseListAndResetSelector();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
//
async function batchCleanOut() {
const title = t('case.batchDeleteCaseTip', {
count: batchParams.value.currentSelectCount || tableSelected.value.length,
});
openModal({
type: 'error',
title,
content: t('case.recycle.cleanOutDeleteOnRecycleTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await batchDeleteRecycleCase(getBatchParams());
Message.success(t('common.deleteSuccess'));
loadCaseListAndResetSelector();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
function handleTableSelect(arr: (string | number)[]) {
tableSelected.value = arr;
}
//
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
tableSelected.value = params?.selectedIds || [];
batchParams.value = params;
switch (event.eventTag) {
case 'batchRecover':
batchRecover();
break;
case 'batchCleanOut':
batchCleanOut();
break;
default:
break;
}
}
</script>
<style lang="less" scoped>
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
border-color: transparent !important;
.arco-input::placeholder {
@apply invisible;
}
.arco-select-view-icon {
@apply invisible;
}
.arco-select-view-value {
color: var(--color-text-brand);
}
}
}
</style>

View File

@ -9,12 +9,15 @@
:protocol="protocol"
/>
</a-tab-pane>
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"></a-tab-pane>
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane">
<api-case :active-module="props.activeModule" :offspring-ids="props.offspringIds" :protocol="protocol"></api-case>
</a-tab-pane>
</a-tabs>
</template>
<script setup lang="ts">
import api from './api/apiTable.vue';
import apiCase from './case/caseTable.vue';
import useAppStore from '@/store/modules/app';

View File

@ -165,9 +165,16 @@ export default {
'case.passRate': 'Pass Rate',
'case.passRateTip': 'Number of success case executions/Total number of case executions *%',
'case.batchModalSubTitle': '({count} cases selected)',
'case.batchDeleteCaseTipTitle': 'Are you sure you want to delete {name} ',
'case.batchRecoverCaseTipTitle': 'Are you sure you want to recover {name} ',
'case.batchDeleteCaseTip': 'Are you sure you want to delete {count} selected cases?',
'case.recycle.cleanOutDeleteOnRecycleTip':
'After deletion, the case cannot be restored. Please operate with caution!',
'case.deleteCaseTip':
'Deleting an case will result in the execution failure of the test task that references the use case. Please be cautious!',
'apiTestManagement.click': 'Click',
'apiTestManagement.getResponse': 'Get response content',
'case.batchRecoverCaseTip': 'Are you sure you want to recover {count} selected cases?',
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
'case.recycle.confirmRecovery': 'Confirm recovery',
};

View File

@ -162,5 +162,11 @@ export default {
'case.passRateTip': '用例执行success数/用例执行总数*%',
'case.batchModalSubTitle': '(已选 {count} 个用例)',
'case.batchDeleteCaseTip': '确认删除已选中的 {count} 个用例吗?',
'case.batchDeleteCaseTipTitle': '确认删除 {name} 吗?',
'case.batchRecoverCaseTipTitle': '确认恢复 {name} 吗?',
'case.recycle.cleanOutDeleteOnRecycleTip': '删除后,用例无法恢复,请谨慎操作!',
'case.deleteCaseTip': '删除用例会导致引用了该用例的测试任务执行失败,请谨慎操作!',
'case.batchRecoverCaseTip': '确认恢复已选中的 {count} 个用例吗?',
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
'case.recycle.confirmRecovery': '确认恢复',
};