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, BatchCleanOutApiUrl,
BatchDeleteCaseUrl, BatchDeleteCaseUrl,
BatchDeleteDefinitionUrl, BatchDeleteDefinitionUrl,
BatchDeleteRecycleCaseUrl,
BatchEditCaseUrl, BatchEditCaseUrl,
BatchMoveDefinitionUrl, BatchMoveDefinitionUrl,
BatchRecoverApiUrl, BatchRecoverApiUrl,
BatchRecoverCaseUrl,
BatchUpdateDefinitionUrl, BatchUpdateDefinitionUrl,
CasePageUrl, CasePageUrl,
CheckDefinitionScheduleUrl, CheckDefinitionScheduleUrl,
@ -23,6 +25,7 @@ import {
DeleteMockUrl, DeleteMockUrl,
DeleteModuleUrl, DeleteModuleUrl,
DeleteRecycleApiUrl, DeleteRecycleApiUrl,
DeleteRecycleCaseUrl,
GetDefinitionDetailUrl, GetDefinitionDetailUrl,
GetDefinitionScheduleUrl, GetDefinitionScheduleUrl,
GetEnvModuleUrl, GetEnvModuleUrl,
@ -34,8 +37,10 @@ import {
ImportDefinitionUrl, ImportDefinitionUrl,
MoveModuleUrl, MoveModuleUrl,
OperationHistoryUrl, OperationHistoryUrl,
RecoverCaseUrl,
RecoverDefinitionUrl, RecoverDefinitionUrl,
RecoverOperationHistoryUrl, RecoverOperationHistoryUrl,
RecycleCasePageUrl,
SaveOperationHistoryUrl, SaveOperationHistoryUrl,
SortCaseUrl, SortCaseUrl,
SortDefinitionUrl, SortDefinitionUrl,
@ -345,6 +350,34 @@ export function dragSort(data: DragSortParams) {
return MSR.post({ url: SortCaseUrl, data }); 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) { export function addCase(data: AddApiCaseParams) {
return MSR.post({ url: AddCaseUrl, data }); 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 CasePageUrl = '/api/case/page'; // 接口用例列表
export const UpdateCaseStatusUrl = '/api/case/update-status'; // 接口用例更新状态 export const UpdateCaseStatusUrl = '/api/case/update-status'; // 接口用例更新状态
export const UpdateCasePriorityUrl = '/api/case/update-priority'; // 接口用例更新等级 export const UpdateCasePriorityUrl = '/api/case/update-priority'; // 接口用例更新等级
export const DeleteCaseUrl = '/api/case/delete'; // 删除接口用例 export const DeleteCaseUrl = '/api/case/delete-to-gc'; // 删除接口用例
export const BatchDeleteCaseUrl = '/api/case/batch/delete'; // 批量删除接口用例 export const BatchDeleteCaseUrl = '/api/case/batch/delete-to-gc'; // 批量删除接口用例
export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例 export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例
export const SortCaseUrl = '/api/case/edit/pos'; // 接口用例拖拽 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'; // 添加用例 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" :protocol="protocol"
/> />
</a-tab-pane> </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> </a-tabs>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import api from './api/apiTable.vue'; import api from './api/apiTable.vue';
import apiCase from './case/caseTable.vue';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';

View File

@ -165,9 +165,16 @@ export default {
'case.passRate': 'Pass Rate', 'case.passRate': 'Pass Rate',
'case.passRateTip': 'Number of success case executions/Total number of case executions *%', 'case.passRateTip': 'Number of success case executions/Total number of case executions *%',
'case.batchModalSubTitle': '({count} cases selected)', '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.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': 'case.deleteCaseTip':
'Deleting an case will result in the execution failure of the test task that references the use case. Please be cautious!', '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.click': 'Click',
'apiTestManagement.getResponse': 'Get response content', '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.passRateTip': '用例执行success数/用例执行总数*%',
'case.batchModalSubTitle': '(已选 {count} 个用例)', 'case.batchModalSubTitle': '(已选 {count} 个用例)',
'case.batchDeleteCaseTip': '确认删除已选中的 {count} 个用例吗?', 'case.batchDeleteCaseTip': '确认删除已选中的 {count} 个用例吗?',
'case.batchDeleteCaseTipTitle': '确认删除 {name} 吗?',
'case.batchRecoverCaseTipTitle': '确认恢复 {name} 吗?',
'case.recycle.cleanOutDeleteOnRecycleTip': '删除后,用例无法恢复,请谨慎操作!',
'case.deleteCaseTip': '删除用例会导致引用了该用例的测试任务执行失败,请谨慎操作!', 'case.deleteCaseTip': '删除用例会导致引用了该用例的测试任务执行失败,请谨慎操作!',
'case.batchRecoverCaseTip': '确认恢复已选中的 {count} 个用例吗?',
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
'case.recycle.confirmRecovery': '确认恢复',
}; };