feat(接口测试): 接口测试报告列表

This commit is contained in:
fit2-zhao 2024-03-09 23:12:07 +08:00 committed by Craftsman
parent 5409468da3
commit c1398046b7
8 changed files with 582 additions and 94 deletions

View File

@ -0,0 +1,18 @@
import MSR from '@/api/http';
import * as orgUrl from '@/api/requrls/setting/organizationAndProject';
import type { TableQueryParams } from '@/models/common';
// 报告列表
export function reportList(data: TableQueryParams) {
return MSR.post({ url: orgUrl.postProjectTableUrl, data });
}
// 删除报告
export function reportDelete(id: string) {}
// 重命名
export function reportRename(id: string, reportName: string) {}
// 批量删除
export function reportBathDelete(ids: Array<string>) {}
export default {};

View File

@ -65,6 +65,13 @@ export const pathMap: PathMapItem[] = [
permission: [], permission: [],
level: MENU_LEVEL[2], level: MENU_LEVEL[2],
}, },
{
key: 'API_TEST_REPORT', // 接口测试-接口测试报告
locale: 'menu.apiTest.report',
route: RouteEnum.API_TEST_REPORT,
permission: [],
level: MENU_LEVEL[2],
},
], ],
}, },
{ {

View File

@ -0,0 +1,90 @@
// 模板展示字段icon
export enum ReportEnum {
API_SCENARIO_REPORT = 'API_SCENARIO_REPORT',
API_REPORT = 'API_REPORT',
}
export enum ReportType {
ALL = 'ALL',
COLLECTION = 'COLLECTION',
INDEPENDENT = 'INDEPENDENT',
}
export enum TriggerModeLabel {
SCHEDULE = 'project.taskCenter.scheduledTask', // 定时任务
MANUAL = 'project.taskCenter.manualExecution', // 手动执行
API = 'project.taskCenter.interfaceCall', // 接口调用
BATCH = 'project.taskCenter.batchExecution', // 批量执行
}
export const ReportStatus = {
[ReportEnum.API_REPORT]: {
SUCCESS: {
icon: 'icon-icon_succeed_colorful',
label: 'report.successful',
},
ERROR: {
icon: 'icon-icon_close_colorful',
label: 'report.failure',
},
FAKE_ERROR: {
icon: 'icon-icon_warning_colorful',
label: 'report.falseAlarm',
},
STOPPED: {
icon: 'icon-icon_block_filled',
label: 'report.stop',
color: '!var(--color-text-input-border)',
},
RUNNING: {
icon: 'icon-icon_testing',
label: 'report.inExecution',
color: '!text-[rgb(var(--link-6))]',
},
RERUNNING: {
icon: 'icon-icon_testing',
label: 'report.rerun',
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
label: 'report.queuing',
color: '!text-[rgb(var(--link-6))]',
},
},
[ReportEnum.API_SCENARIO_REPORT]: {
SUCCESS: {
icon: 'icon-icon_succeed_colorful',
label: 'report.successful',
},
ERROR: {
icon: 'icon-icon_close_colorful',
label: 'report.failure',
},
FAKE_ERROR: {
icon: 'icon-icon_warning_colorful',
label: 'report.falseAlarm',
},
STOPPED: {
icon: 'icon-icon_block_filled',
label: 'report.stop',
color: 'var(--color-text-input-border)',
},
RUNNING: {
icon: 'icon-icon_testing',
label: 'report.inExecution',
color: '!text-[rgb(var(--link-6))]',
},
RERUNNING: {
icon: 'icon-icon_testing',
label: 'report.rerun',
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
label: 'report.queuing',
color: '!text-[rgb(var(--link-6))]',
},
},
};
export default {};

View File

@ -0,0 +1,288 @@
<template>
<div class="px-[16px]">
<div class="mb-4 flex items-center justify-between">
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="All">{{ t('report.all') }}</a-radio>
<a-radio value="Independent">{{ t('report.independent') }}</a-radio>
<a-radio value="Collection">{{ t('report.collection') }}</a-radio>
</a-radio-group>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('project.menu.nameSearch')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
/>
</div>
<!-- 报告列表 -->
<ms-base-table
v-bind="propsRes"
ref="tableRef"
:action-config="tableBatchActions"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<!-- 报告类型筛选 -->
<template #typeFilter="{ columnConfig }">
<a-trigger v-model:popup-visible="typeFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="typeFilterVisible = true">
<div class="font-medium">
{{ t(columnConfig.title as string) }}
</div>
<icon-down :class="typeFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusListFilters" direction="vertical" size="small">
<a-checkbox v-for="key of statusFilters" :key="key" :value="key">
<ExecutionStatus :module-type="props.moduleType" :status="key" />
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<!-- 报告结果筛选 -->
<template #statusFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="statusFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="statusFilterVisible = true">
<div class="font-medium">
{{ t(columnConfig.title as string) }}
</div>
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusListFilters" direction="vertical" size="small">
<a-checkbox v-for="key of statusFilters" :key="key" :value="key">
<ExecutionStatus :module-type="props.moduleType" :status="key" />
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #status="{ record }">
<ExecutionStatus :module-type="props.moduleType" :status="record.status" />
</template>
<template #triggerMode="{ record }">
<span>{{ t(TriggerModeLabel[record.triggerMode]) }}</span>
</template>
<template #operationTime="{ record }">
<span>{{ dayjs(record.operationTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #operation="{ record }">
<MsButton class="!mr-0" @click="handleDelete(record)">{{ t('ms.comment.delete') }}</MsButton>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
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 ExecutionStatus from './reportStatus.vue';
import { reportDelete, reportList, reportRename } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
import { BatchApiParams } from '@/models/common';
import { ReportEnum, ReportStatus, ReportType, TriggerModeLabel } from '@/enums/reportEnum';
import { ColumnEditTypeEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = defineProps<{
moduleType: keyof typeof ReportEnum;
name: string;
}>();
const keyword = ref<string>('');
const statusFilterVisible = ref(false);
const typeFilterVisible = ref(false);
const statusListFilters = ref<string[]>(Object.keys(ReportStatus[props.moduleType]));
type ReportShowType = 'All' | 'Independent' | 'Collection';
const showType = ref<ReportShowType>('All');
const columns: MsTableColumn = [
{
title: 'report.name',
dataIndex: 'name',
slotName: 'name',
width: 200,
showInTable: true,
showTooltip: true,
editType: ColumnEditTypeEnum.INPUT, // hasAnyPermission(['API_TEST_REPORT:READ+UPDATE']) ? ColumnEditTypeEnum.INPUT : undefined,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
ellipsis: true,
showDrag: false,
columnSelectorDisabled: true,
},
{
title: 'report.type',
slotName: 'reportType',
dataIndex: 'reportType',
width: 150,
showDrag: true,
titleSlotName: 'typeFilter',
},
{
title: 'report.result',
dataIndex: 'reportResult',
slotName: 'reportResult',
titleSlotName: 'statusFilter',
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'report.trigger.mode',
dataIndex: 'triggerMode',
slotName: 'triggerMode',
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'report.operator',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'report.operating',
dataIndex: 'createTime',
slotName: 'createTime',
width: 180,
showDrag: true,
},
{
title: 'common.operation',
slotName: 'operation',
dataIndex: 'operation',
width: 100,
fixed: 'right',
},
];
const rename = async (record: any) => {
try {
await reportRename(record.id, record.name);
Message.success(t('common.updateSuccess'));
return true;
} catch (error) {
return false;
}
};
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
reportList,
{
columns,
scroll: {
x: '100%',
},
showSetting: false,
selectable: true,
heightUsed: 330,
showSelectAll: true,
},
undefined,
rename
);
function initData() {
setLoadListParams({
keyword: keyword.value,
moduleType: props.moduleType,
filter: { status: statusListFilters.value },
});
loadList();
}
const tableBatchActions = {
baseAction: [
{
label: 'report.batch.delete',
eventTag: 'batchStop',
},
],
};
const batchParams = ref<BatchApiParams>({
selectIds: [],
selectAll: false,
excludeIds: [] as string[],
condition: {},
});
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = { ...params, selectIds: params?.selectedIds || [], condition: {} };
if (event.eventTag === 'batchDelete') {
//
}
}
function searchList() {
resetSelector();
initData();
}
function handleDelete(id: string) {
Message.success(t('apiTestDebug.deleteSuccess'));
}
onBeforeMount(() => {
initData();
});
const statusFilters = computed(() => {
return Object.keys(ReportStatus[props.moduleType]);
});
function handleFilterHidden(val: boolean) {
if (!val) {
initData();
}
}
function changeShowType(val: string | number | boolean) {
showType.value = val as ReportShowType;
if (val === ReportType.COLLECTION) {
console.log('Collection');
} else if (val === ReportType.INDEPENDENT) {
console.log('Independent');
} else {
console.log('All');
}
}
watch(
() => props.moduleType,
(val) => {
if (val) {
resetSelector();
initData();
}
}
);
</script>
<style scoped></style>

View File

@ -0,0 +1,103 @@
<template>
<div class="flex items-center justify-start">
<MsIcon :type="getExecutionResult().icon" :class="getExecutionResult()?.color" size="14" />
<span class="ml-1">{{ t(getExecutionResult().label) }}</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { ReportEnum } from '@/enums/reportEnum';
const { t } = useI18n();
const props = defineProps<{
status: string;
moduleType: keyof typeof ReportEnum;
}>();
export interface IconType {
icon: string;
label: string;
color?: string;
}
const iconTypeStatus = ref({
[ReportEnum.API_REPORT]: {
SUCCESS: {
icon: 'icon-icon_succeed_colorful',
label: 'report.successful',
},
ERROR: {
icon: 'icon-icon_close_colorful',
label: 'report.failure',
},
FAKE_ERROR: {
icon: 'icon-icon_warning_colorful',
label: 'report.fake.error',
},
STOPPED: {
icon: 'icon-icon_block_filled',
label: 'report.stopped',
color: '!text-[var(--color-text-input-border)]',
},
RUNNING: {
icon: 'icon-icon_testing',
label: 'report.status.running',
color: '!text-[rgb(var(--link-6))]',
},
RERUNNING: {
icon: 'icon-icon_testing',
label: 'report.status.rerunning',
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
label: 'report.status.pending',
color: '!text-[rgb(var(--link-6))]',
},
},
[ReportEnum.API_SCENARIO_REPORT]: {
SUCCESS: {
icon: 'icon-icon_succeed_colorful',
label: 'report.successful',
},
ERROR: {
icon: 'icon-icon_close_colorful',
label: 'report.failure',
},
FAKE_ERROR: {
icon: 'icon-icon_warning_colorful',
label: 'report.fake.error',
},
STOPPED: {
icon: 'icon-icon_block_filled',
label: 'report.stopped',
color: '!text-[var(--color-text-input-border)]',
},
RUNNING: {
icon: 'icon-icon_testing',
label: 'report.status.running',
color: '!text-[rgb(var(--link-6))]',
},
RERUNNING: {
icon: 'icon-icon_testing',
label: 'report.status.rerunning',
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
label: 'report.status.pending',
color: '!text-[rgb(var(--link-6))]',
},
},
});
function getExecutionResult(): IconType {
return iconTypeStatus.value[props.moduleType][props.status];
}
</script>
<style scoped></style>

View File

@ -1,22 +1,49 @@
<template> <template>
<!-- <MsCard class="mb-[16px]" :title="props.title" hide-back hide-footer auto-height no-content-padding no-bottom-radius> <MsCard simple no-content-padding>
<a-tabs v-model:active-key="innerTab" class="no-content"> <a-tabs v-model:active-key="activeTab" class="no-content">
<a-tab-pane v-for="item of tabList" :key="item.key" :title="item.title" /> <a-tab-pane v-for="item of realTabList" :key="item.value" :title="item.label" />
</a-tabs> </a-tabs>
</MsCard> --> <a-divider margin="0" class="!mb-[16px]"></a-divider>
<div></div> <!-- 报告列表-->
<ReportList :name="listName" :module-type="activeTab" />
</MsCard>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import ReportList from './component/reportList.vue';
const tabList = [ import { useI18n } from '@/hooks/useI18n';
import { ReportEnum } from '@/enums/reportEnum';
const { t } = useI18n();
const activeTab = ref<keyof typeof ReportEnum>(ReportEnum.API_SCENARIO_REPORT);
const realTabList = ref([
{ {
value: '', value: ReportEnum.API_SCENARIO_REPORT,
label: t('report.api.scenario'),
}, },
]; {
value: ReportEnum.API_REPORT,
label: t('report.api.case'),
},
]);
const rightTabList = computed(() => {
return realTabList.value;
});
const listName = computed(() => {
return rightTabList.value.find((item) => item.value === activeTab.value)?.label || '';
});
</script> </script>
<style scoped></style> <style scoped lang="less">
:deep(.arco-tabs-content) {
padding-top: 0;
}
</style>

View File

@ -1 +1,21 @@
export default {}; export default {
'report.api.case': 'Case report',
'report.api.scenario': 'Scenario report',
'report.all': 'All',
'report.independent': 'Independent report',
'report.collection': 'Collection report',
'report.name': 'Report name',
'report.type': 'Report type',
'report.result': 'Report result',
'report.trigger.mode': 'Trigger mode',
'report.operator': 'Create User',
'report.operating': 'Create time',
'report.batch.delete': 'Batch delete',
'report.successful': 'Successful',
'report.failure': 'failure',
'report.fake.error': 'Fake error',
'report.status.running': 'Running',
'report.status.rerunning': 'Rerunning',
'report.status.pending': 'Pending',
'report.stopped': 'Stopped',
};

View File

@ -1,86 +1,21 @@
export default { export default {
'apiTestManagement.newApi': '新建接口', 'report.api.case': '用例报告',
'apiTestManagement.importApi': '导入接口', 'report.api.scenario': '场景报告',
'apiTestManagement.fileImport': '文件导入', 'report.all': '全部',
'apiTestManagement.timeImport': '定时导入', 'report.independent': '独立报告',
'apiTestManagement.addSubModule': '添加子模块', 'report.collection': '集合报告',
'apiTestManagement.allApi': '全部接口', 'report.name': '报告名称',
'apiTestManagement.searchTip': '请输入模块/接口名称', 'report.type': '报告类型',
'apiTestManagement.moveSearchTip': '请输入模块名称搜索', 'report.result': '报告结果',
'apiTestManagement.noMatchModule': '暂无匹配的模块/接口', 'report.trigger.mode': '触发方式',
'apiTestManagement.execute': '执行', 'report.operator': '创建人',
'apiTestManagement.share': '分享 API', 'report.operating': '创建时间',
'apiTestManagement.shareModule': '分享模块', 'report.batch.delete': '批量删除',
'apiTestManagement.doc': '文档', 'report.successful': '成功',
'apiTestManagement.closeAll': '关闭全部tab', 'report.failure': '失败',
'apiTestManagement.closeOther': '关闭其他tab', 'report.fake.error': '误报',
'apiTestManagement.showSubdirectory': '显示子目录用例', 'report.status.running': '执行中',
'apiTestManagement.searchPlaceholder': '输入 ID/名称/api路径搜索', 'report.status.rerunning': '重跑中',
'apiTestManagement.apiName': '接口名称', 'report.status.pending': '排队中',
'apiTestManagement.apiType': '请求类型', 'report.stopped': '停止',
'apiTestManagement.apiStatus': '状态',
'apiTestManagement.responsiblePerson': '责任人',
'apiTestManagement.path': '路径',
'apiTestManagement.version': '版本',
'apiTestManagement.createTime': '创建时间',
'apiTestManagement.updateTime': '更新时间',
'apiTestManagement.deprecate': '已废弃',
'apiTestManagement.processing': '进行中',
'apiTestManagement.debugging': '联调中',
'apiTestManagement.done': '已完成',
'apiTestManagement.deleteApiTipTitle': '确认删除 {name} 吗?',
'apiTestManagement.deleteApiTip': '删除后,接口将放入回收站,可在回收站内进行数据恢复',
'apiTestManagement.batchDeleteApiTip': '确认删除已选中的 {count} 个接口吗?',
'apiTestManagement.batchModalSubTitle': '(已选 {count} 个接口)',
'apiTestManagement.chooseAttr': '选择属性',
'apiTestManagement.attrRequired': '属性不能为空',
'apiTestManagement.batchUpdate': '批量更新为',
'apiTestManagement.valueRequired': '属性值不能为空',
'apiTestManagement.batchMoveConfirm': '移动至所选模块',
'apiTestManagement.belongModule': '所属模块',
'apiTestManagement.importMode': '导入模式',
'apiTestManagement.importModeTip1': '覆盖:',
'apiTestManagement.importModeTip2': '1.同一接口请求类型+路径一致,请求参数内容不一致则覆盖',
'apiTestManagement.importModeTip3': '2.同一接口请求类型+路径一致,请求参数内容一致不做变更',
'apiTestManagement.importModeTip4': '3.非同一接口,请求类型+路径一致,则新增',
'apiTestManagement.importModeTip5': '不覆盖:',
'apiTestManagement.importModeTip6': '1.同一接口请求类型+路径一致,则不做变更',
'apiTestManagement.importModeTip7': '2.非同一接口请求类型+路径一致,则新增',
'apiTestManagement.cover': '覆盖',
'apiTestManagement.uncover': '不覆盖',
'apiTestManagement.moreSetting': '更多设置',
'apiTestManagement.importType': '导入方式',
'apiTestManagement.urlImport': 'URL 导入',
'apiTestManagement.syncImportCase': '同步导入接口用例',
'apiTestManagement.syncUpdateDirectory': '同步更新接口所在目录',
'apiTestManagement.importSwaggerFileTip1': '支持 Swagger 3.0 版本的 json 文件,',
'apiTestManagement.importSwaggerFileTip2': '2.0 的文件建议自行在官网转换成 3.0 再进行导入',
'apiTestManagement.importSwaggerFileTip3': ',大小不超过 50M',
'apiTestManagement.urlImportPlaceholder': '请输入OpenAPI/Swagger URL',
'apiTestManagement.swaggerURLRequired': 'SwaggerURL 不能为空',
'apiTestManagement.basicAuth': 'Basic Auth 认证',
'apiTestManagement.account': '账号',
'apiTestManagement.accountRequired': '账号不能为空',
'apiTestManagement.password': '密码',
'apiTestManagement.passwordRequired': '密码不能为空',
'apiTestManagement.taskName': '任务名称',
'apiTestManagement.taskNamePlaceholder': '请输入任务名称',
'apiTestManagement.taskNameRequired': '任务名称不能为空',
'apiTestManagement.syncFrequency': '同步频率',
'apiTestManagement.timeTaskList': '定时任务列表',
'apiTestManagement.timeTaskHour': '(每小时)',
'apiTestManagement.timeTaskSixHour': '(每 6 小时)',
'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)',
'apiTestManagement.timeTaskDay': '(每天)',
'apiTestManagement.customFrequency': '自定义频率',
'apiTestManagement.case': '用例',
'apiTestManagement.definition': '定义',
'apiTestManagement.addDependency': '添加依赖关系',
'apiTestManagement.preDependency': '前置依赖',
'apiTestManagement.addPreDependency': '添加前置依赖',
'apiTestManagement.postDependency': '后置依赖',
'apiTestManagement.addPostDependency': '添加后置依赖',
'apiTestManagement.saveAsCase': '保存为新用例',
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
'apiTestManagement.apiNameRequired': '接口名称不能为空',
}; };