feat(接口场景): 场景步骤 3%&部分 bug 解决

This commit is contained in:
baiqi 2024-03-15 21:28:29 +08:00 committed by Craftsman
parent ffbe77bb73
commit 5e17455904
21 changed files with 779 additions and 169 deletions

View File

@ -23,6 +23,7 @@
}>();
const emit = defineEmits<{
(e: 'update:project', val: string): void;
(e: 'change', val: string): void;
}>();
const appStore = useAppStore();
@ -61,6 +62,7 @@
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
emit('update:project', value as string);
emit('change', value as string);
}
</script>

View File

@ -57,7 +57,7 @@
>
<icon-drag-dot-vertical class="absolute left-[-3px] top-[50%] w-[14px]" size="14" />
</div>
<a-scrollbar class="h-full overflow-y-auto">
<a-scrollbar class="ms-drawer-body-scrollbar">
<div class="ms-drawer-body">
<slot>
<MsDescription
@ -294,6 +294,15 @@
}
}
.ms-drawer {
.arco-drawer-body {
@apply overflow-hidden;
}
.ms-drawer-body-scrollbar {
@apply h-full w-full overflow-auto;
min-width: 680px;
min-height: 500px;
}
.ms-drawer-body {
@apply h-full;
}

View File

@ -0,0 +1,26 @@
import { RouteRecordName, useRouter } from 'vue-router';
import { useAppStore } from '@/store';
/**
*
* @param name
* @param query
*/
export default function useOpenNewPage() {
const appStore = useAppStore();
const router = useRouter();
function openNewPage(name: RouteRecordName | undefined, query = {}) {
const queryParams = new URLSearchParams(query).toString();
window.open(
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${
appStore.currentProjectId
}&${queryParams}`
);
}
return {
openNewPage,
};
}

View File

@ -1,16 +1,19 @@
<template>
<MsTag
v-if="props.isTag"
:self-style="{
border: `1px solid ${props.tagBackgroundColor || methodColor}`,
color: props.tagTextColor || methodColor,
backgroundColor: props.tagBackgroundColor || 'white',
}"
:size="props.tagSize"
>
{{ props.method }}
</MsTag>
<div v-else class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
<div v-if="props.method">
<MsTag
v-if="props.isTag"
:self-style="{
border: `1px solid ${props.tagBackgroundColor || methodColor}`,
color: props.tagTextColor || methodColor,
backgroundColor: props.tagBackgroundColor || 'white',
}"
:size="props.tagSize"
>
{{ props.method }}
</MsTag>
<div v-else class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
</div>
<div v-else>-</div>
</template>
<script setup lang="ts">
@ -20,7 +23,7 @@
const props = withDefaults(
defineProps<{
method: RequestMethods;
method?: RequestMethods;
isTag?: boolean;
tagSize?: Size;
tagBackgroundColor?: string;
@ -60,8 +63,11 @@
];
const methodColor = computed(() => {
const colorMap = colorMaps.find((item) => item.includes.includes(props.method));
return colorMap?.color || 'rgb(var(--link-7))'; // key
if (props.method) {
const colorMap = colorMaps.find((item) => item.includes.includes(props.method!));
return colorMap?.color || 'rgb(var(--link-7))'; // key
}
return 'rgb(var(--link-7))';
});
</script>

View File

@ -144,8 +144,13 @@
}
});
async function handleFileChange() {
async function handleFileChange(files: MsFileItem[]) {
if (!props.uploadTempFileApi) return;
if (files.length === 0) {
innerParams.value.binaryBody.file = undefined;
emit('change');
return;
}
try {
if (fileList.value[0]?.local && fileList.value[0].file) {
appStore.showLoading();

View File

@ -563,10 +563,11 @@
);
export interface RequestCustomAttr {
type: 'api' | 'case' | 'mock' | 'doc'; // tab api
isNew: boolean;
protocol: string;
activeTab: RequestComposition;
mode?: 'definition' | 'debug' | 'case';
mode?: 'definition' | 'debug'; // / tab
executeLoading: boolean; // loading
isCopy?: boolean; //
isExecute?: boolean; //
@ -673,6 +674,7 @@
];
// tab
const contentTabList = computed(() => {
// HTTP tabs
if (isHttpProtocol.value) {
if (props.isDefinition) {
//
@ -683,6 +685,7 @@
//
return httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
}
// tabs
if (props.isDefinition) {
//
return requestVModel.value.mode === 'definition'
@ -1203,7 +1206,7 @@
await initProtocolList();
}
await initPluginScript();
} else {
} else if (protocolOptions.value.length === 0) {
await initProtocolList();
}
if (props.request.isExecute && !requestVModel.value.executeLoading) {

View File

@ -363,8 +363,13 @@
}
});
async function handleFileChange() {
async function handleFileChange(files: MsFileItem[]) {
if (!props.uploadTempFileApi) return;
if (files.length === 0) {
activeResponse.value.binaryBody.file = undefined;
emit('change');
return;
}
try {
if (fileList.value[0]?.local && fileList.value[0].file) {
appStore.showLoading();

View File

@ -150,6 +150,7 @@
const initDefaultId = `debug-${Date.now()}`;
const defaultDebugParams: RequestParam = {
type: 'api',
id: initDefaultId,
moduleId: 'root',
protocol: 'HTTP',

View File

@ -11,7 +11,7 @@
/>
</div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<a-tabs v-model:active-key="definitionActiveKey" animation lazy-load class="ms-api-tab-nav">
<a-tabs v-model:active-key="activeApiTab.definitionActiveKey" animation lazy-load class="ms-api-tab-nav">
<a-tab-pane
v-if="!activeApiTab.isNew"
key="preview"
@ -19,7 +19,7 @@
class="ms-api-tab-pane"
>
<preview
v-if="definitionActiveKey === 'preview'"
v-if="activeApiTab.definitionActiveKey === 'preview'"
:detail="activeApiTab"
:module-tree="props.moduleTree"
:protocols="protocols"
@ -116,7 +116,6 @@
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const definitionActiveKey = ref('definition');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const appStore = useAppStore();
@ -145,6 +144,8 @@
const initDefaultId = `definition-${Date.now()}`;
const defaultDefinitionParams: RequestParam = {
type: 'api',
definitionActiveKey: 'definition',
id: initDefaultId,
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
protocol: 'HTTP',
@ -220,12 +221,10 @@
label: t('apiTestManagement.newApi'),
id,
isNew: !defaultProps?.id, // tabidid
definitionActiveKey: !defaultProps ? 'definition' : 'preview',
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
if (!defaultProps) {
definitionActiveKey.value = 'definition';
}
}
const apiTableRef = ref<InstanceType<typeof apiTable>>();
@ -257,7 +256,6 @@
loading.value = true;
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
const name = isCopy ? `copy-${res.name}` : res.name;
definitionActiveKey.value = isCopy || isExecute ? 'definition' : 'preview';
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
@ -276,6 +274,7 @@
id: isCopy ? new Date().getTime() : res.id,
isExecute,
mode: isExecute ? 'debug' : 'definition',
definitionActiveKey: isCopy || isExecute ? 'definition' : 'preview',
...parseRequestBodyResult,
});
nextTick(() => {

View File

@ -409,14 +409,20 @@
}
}
watchEffect(() => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
[activeResponse.value] = previewDetail.value.responseDefinition || [];
if (previewDetail.value.protocol !== 'HTTP') {
//
initPluginScript(previewDetail.value.protocol);
watch(
() => props.detail.id,
() => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
[activeResponse.value] = previewDetail.value.responseDefinition || [];
if (previewDetail.value.protocol !== 'HTTP') {
//
initPluginScript(previewDetail.value.protocol);
}
},
{
immediate: true,
}
});
);
const activeDetailKey = ref(['request', 'response']);

View File

@ -109,27 +109,33 @@
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
watchEffect(() => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
if (props.isCaseDetail) return;
const tableParam = getValidRequestTableParams(previewDetail.value); // props.detail
previewDetail.value = {
...previewDetail.value,
body: {
...previewDetail.value.body,
formDataBody: {
formValues: tableParam.formDataBodyTableParams,
watch(
() => props.detail.id,
() => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
if (props.isCaseDetail) return;
const tableParam = getValidRequestTableParams(previewDetail.value); // props.detail
previewDetail.value = {
...previewDetail.value,
body: {
...previewDetail.value.body,
formDataBody: {
formValues: tableParam.formDataBodyTableParams,
},
wwwFormBody: {
formValues: tableParam.wwwFormBodyTableParams,
},
},
wwwFormBody: {
formValues: tableParam.wwwFormBodyTableParams,
},
},
headers: tableParam.headers,
rest: tableParam.rest,
query: tableParam.query,
responseDefinition: tableParam.response,
};
});
headers: tableParam.headers,
rest: tableParam.rest,
query: tableParam.query,
responseDefinition: tableParam.response,
};
},
{
immediate: true,
}
);
const description = computed(() => {
const commonDescription = [

View File

@ -9,6 +9,8 @@
</template>
<script setup lang="ts">
import preview from '@/views/api-test/management/components/management/api/preview/index.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import useAppStore from '@/store/modules/app';
@ -17,8 +19,6 @@
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const preview = defineAsyncComponent(() => import('../api/preview/index.vue'));
const props = defineProps<{
moduleTree: ModuleTreeNode[]; //
}>();

View File

@ -8,7 +8,7 @@
@open-case-tab="openCaseTab"
/>
</div>
<div v-show="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<caseDetail :active-api-tab="activeApiTab" :module-tree="props.moduleTree" />
</div>
</div>
@ -18,7 +18,6 @@
import { cloneDeep } from 'lodash-es';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import caseDetail from './caseDetail.vue';
import caseTable from './caseTable.vue';
import { getCaseDetail } from '@/api/modules/api-test/management';
@ -31,6 +30,9 @@
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
//
const caseDetail = defineAsyncComponent(() => import('./caseDetail.vue'));
const props = defineProps<{
activeModule: string;
protocol: string;
@ -47,6 +49,7 @@
const initDefaultId = `case-${Date.now()}`;
const defaultCaseParams: RequestParam = {
type: 'case',
id: initDefaultId,
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
protocol: 'HTTP',
@ -105,7 +108,7 @@
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
mode: 'case',
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //

View File

@ -37,7 +37,7 @@
</a-select>
</div>
<api
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.mode === 'definition'"
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.type === 'api'"
ref="apiRef"
v-model:active-api-tab="activeApiTab"
v-model:api-tabs="apiTabs"
@ -47,7 +47,7 @@
:module-tree="props.moduleTree"
/>
<apiCase
v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.mode === 'case'"
v-if="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'"
v-model:api-tabs="apiTabs"
v-model:active-api-tab="activeApiTab"
:active-module="props.activeModule"
@ -133,7 +133,7 @@
watch(
() => activeApiTab.value.id,
() => {
if (typeof setActiveApi === 'function' && !activeApiTab.value.isNew) {
if (typeof setActiveApi === 'function' && !activeApiTab.value.isNew && activeApiTab.value.type === 'api') {
// tab tab
setActiveApi(activeApiTab.value);
}

View File

@ -30,7 +30,6 @@
<script lang="ts" setup>
import { provide } from 'vue';
import { useRouter } from 'vue-router';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
@ -38,12 +37,8 @@
import moduleTree from './components/moduleTree.vue';
import management from './components/recycle/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { ModuleTreeNode } from '@/models/common';
const { t } = useI18n();
const router = useRouter();
const activeModule = ref<string>('all');
const folderTree = ref<ModuleTreeNode[]>([]);
const folderTreePathMap = ref<Record<string, any>>({});

View File

@ -1,101 +0,0 @@
<template>
<MsDrawer v-model:visible="visible" :title="t('apiScenario.importSystemApi')" :width="1200">
<div class="flex h-full flex-col overflow-hidden">
<a-tabs v-model:active-key="activeKey">
<a-tab-pane key="api" :title="t('apiScenario.api')" />
<a-tab-pane key="case" :title="t('apiScenario.case')" />
<a-tab-pane key="scenario" :title="t('apiScenario.scenario')" />
</a-tabs>
<div class="flex-1">
<div class="flex">
<div class="p-[16px]"></div>
</div>
</div>
</div>
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<div class="second-text">{{ t('apiScenario.sumSelected') }}</div>
<div class="main-text">{{ totalSelected }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.api') }}</div>
<div class="main-text">{{ selectedApis.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.case') }}</div>
<div class="main-text">{{ selectedCases.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.scenario') }}</div>
<div class="main-text">{{ selectedScenarios.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<MsButton v-show="totalSelected > 0" type="text" class="!mr-0 ml-[4px]" @click="clearAll">
{{ t('common.clear') }}
</MsButton>
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="handleCopy">{{ t('common.copy') }}</a-button>
<a-button type="primary" @click="handleQuote">{{ t('common.quote') }}</a-button>
</div>
</div>
</template>
</MsDrawer>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
const emit = defineEmits<{
(e: 'copy', data: any[]): void;
(e: 'quote', data: any[]): void;
}>();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
required: true,
});
const activeKey = ref('api');
const selectedApis = ref<any[]>([]);
const selectedCases = ref<any[]>([]);
const selectedScenarios = ref<any[]>([]);
const totalSelected = computed(() => {
return selectedApis.value.length + selectedCases.value.length + selectedScenarios.value.length;
});
function clearAll() {
selectedApis.value = [];
selectedCases.value = [];
selectedScenarios.value = [];
}
function handleCancel() {
clearAll();
visible.value = false;
}
function handleCopy() {
emit('copy', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
handleCancel();
}
function handleQuote() {
emit('quote', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
handleCancel();
}
</script>
<style lang="less" scoped>
.second-text {
color: var(--color-text-2);
}
.main-text {
color: rgb(var(--primary-5));
}
.arco-tabs-content {
@apply hidden;
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<MsDrawer
v-model:visible="visible"
:title="t('apiScenario.importSystemApi')"
:width="1200"
no-content-padding
disabled-width-drag
>
<div class="h-full w-full overflow-hidden">
<a-tabs v-model:active-key="activeKey" @change="resetModuleAndTable">
<a-tab-pane key="api" :title="t('apiScenario.api')" />
<a-tab-pane key="case" :title="t('apiScenario.case')" />
<a-tab-pane key="scenario" :title="t('apiScenario.scenario')" />
</a-tabs>
<a-divider :margin="0"></a-divider>
<div class="flex">
<div class="w-[300px] border-r p-[16px]">
<div class="flex flex-col">
<div class="mb-[12px] flex items-center gap-[8px]">
<MsProjectSelect v-model:project="currentProject" @change="resetModuleAndTable" />
<a-select
v-model:model-value="protocol"
:options="protocolOptions"
class="w-[90px]"
@change="resetModuleAndTable"
/>
</div>
<moduleTree ref="moduleTreeRef" :type="activeKey" :protocol="protocol" @select="handleModuleSelect" />
</div>
</div>
<div class="table-container">
<apiTable
ref="apiTableRef"
:module="activeModule"
:type="activeKey"
:protocol="protocol"
:project-id="currentProject"
:module-ids="moduleIds"
@select="handleTableSelect"
/>
</div>
</div>
</div>
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<div class="second-text">{{ t('apiScenario.sumSelected') }}</div>
<div class="main-text">{{ totalSelected }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.api') }}</div>
<div class="main-text">{{ selectedApis.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.case') }}</div>
<div class="main-text">{{ selectedCases.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.scenario') }}</div>
<div class="main-text">{{ selectedScenarios.length }}</div>
<a-divider v-show="totalSelected > 0" direction="vertical" :margin="4"></a-divider>
<MsButton v-show="totalSelected > 0" type="text" class="!mr-0 ml-[4px]" @click="clearAll">
{{ t('common.clear') }}
</MsButton>
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="handleCopy">{{ t('common.copy') }}</a-button>
<a-button type="primary" @click="handleQuote">{{ t('common.quote') }}</a-button>
</div>
</div>
</template>
</MsDrawer>
</template>
<script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import moduleTree from './moduleTree.vue';
import apiTable from './table.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
const emit = defineEmits<{
(e: 'copy', data: any[]): void;
(e: 'quote', data: any[]): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
required: true,
});
const activeKey = ref<'api' | 'case' | 'scenario'>('api');
const selectedApis = ref<any[]>([]);
const selectedCases = ref<any[]>([]);
const selectedScenarios = ref<any[]>([]);
const totalSelected = computed(() => {
return selectedApis.value.length + selectedCases.value.length + selectedScenarios.value.length;
});
function handleTableSelect(ids: (string | number)[]) {
if (activeKey.value === 'api') {
selectedApis.value = ids;
} else if (activeKey.value === 'case') {
selectedCases.value = ids;
} else if (activeKey.value === 'scenario') {
selectedScenarios.value = ids;
}
}
const activeModule = ref<MsTreeNodeData>({});
const currentProject = ref(appStore.currentProjectId);
const protocol = ref('HTTP');
const protocolOptions = ref<SelectOptionData[]>([]);
const protocolLoading = ref(false);
async function initProtocolList() {
try {
protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const apiTableRef = ref<InstanceType<typeof apiTable>>();
const moduleIds = ref<(string | number)[]>([]);
function resetModuleAndTable() {
moduleTreeRef.value?.init();
apiTableRef.value?.loadPage(['root']); // id
}
function handleModuleSelect(ids: (string | number)[], node: MsTreeNodeData) {
activeModule.value = node;
moduleIds.value = ids;
apiTableRef.value?.loadPage(ids);
}
function clearAll() {
selectedApis.value = [];
selectedCases.value = [];
selectedScenarios.value = [];
}
function handleCancel() {
clearAll();
visible.value = false;
}
function handleCopy() {
emit('copy', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
handleCancel();
}
function handleQuote() {
emit('quote', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
handleCancel();
}
watch(
() => visible.value,
(val) => {
if (val) {
resetModuleAndTable();
}
}
);
onBeforeMount(() => {
initProtocolList();
});
</script>
<style lang="less" scoped>
.second-text {
color: var(--color-text-2);
}
.main-text {
color: rgb(var(--primary-5));
}
:deep(.arco-tabs-content) {
@apply hidden;
}
.table-container {
@apply overflow-auto;
.ms-scroll-bar();
padding: 16px;
width: calc(100% - 300px);
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<div>
<div class="mb-[12px] flex items-center gap-[8px]">
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiScenario.quoteTreeSearchTip')" allow-clear />
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAll') : t('apiScenario.expandAllStep')">
<a-button
type="outline"
class="expand-btn arco-btn-outline--secondary"
@click="() => (isExpandAll = !isExpandAll)"
>
<MsIcon v-if="isExpandAll" type="icon-icon_comment_collapse_text_input" />
<MsIcon v-else type="icon-icon_comment_expand_text_input" />
</a-button>
</a-tooltip>
</div>
<a-spin class="w-full" :loading="loading">
<MsTree
v-model:selected-keys="selectedKeys"
:data="folderTree"
:keyword="moduleKeyword"
:default-expand-all="isExpandAll"
:expand-all="isExpandAll"
:empty-text="t('apiScenario.quoteTreeNoData')"
:virtual-list-props="{
height: 'calc(100vh - 293px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
}"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="left"
@select="handleNodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">({{ moduleCountMap[nodeData.id] || 0 }})</div>
</div>
</template>
</MsTree>
</a-spin>
</div>
</template>
<script setup lang="ts">
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getModuleCount, getModuleTreeOnlyModules } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common';
const props = withDefaults(
defineProps<{
type: 'api' | 'case' | 'scenario';
protocol: string;
}>(),
{
type: 'api',
}
);
const emit = defineEmits<{
(e: 'select', ids: (string | number)[], node: MsTreeNodeData): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const moduleKeyword = ref('');
const folderTree = ref<ModuleTreeNode[]>([]);
const loading = ref(false);
const isExpandAll = ref(false);
const moduleCountMap = ref<Record<string, number>>({});
const selectedKeys = ref<string[]>([]);
/**
* 初始化模块树
*/
async function initModules() {
try {
loading.value = true;
folderTree.value = await getModuleTreeOnlyModules({
//
keyword: moduleKeyword.value,
protocol: props.protocol,
projectId: appStore.currentProjectId,
moduleIds: [],
});
selectedKeys.value = [folderTree.value[0]?.id];
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
async function initModuleCount() {
try {
moduleCountMap.value = await getModuleCount({
keyword: moduleKeyword.value,
protocol: props.protocol,
projectId: appStore.currentProjectId,
moduleIds: [],
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
function handleNodeSelect(keys: (string | number)[], node: MsTreeNodeData) {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emit('select', [keys[0], ...offspringIds], node);
}
function init() {
initModules();
initModuleCount();
}
defineExpose({
init,
});
</script>
<style lang="less" scoped>
.expand-btn {
padding: 8px;
.arco-icon {
color: var(--color-text-4);
}
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<div class="min-w-[380px]">
<div class="mb-[16px] flex items-center justify-between">
<div class="flex items-center">
<a-tooltip :content="props.module.name">
<div class="one-line-text max-w-[200px]">{{ props.module.name }}</div>
</a-tooltip>
<div>{{ currentTable.propsRes.value.msPagination?.total }}</div>
</div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiScenario.quoteTableSearchTip')"
allow-clear
class="mr-[8px] w-[240px]"
@search="() => loadPage()"
@press-enter="() => loadPage()"
@clear="() => loadPage()"
/>
</div>
<ms-base-table
v-bind="currentTable.propsRes.value"
no-disable
filter-icon-align-left
v-on="currentTable.propsEvent.value"
@selected-change="handleTableSelect"
>
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="methodFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<MsButton type="text" class="arco-btn-text--secondary" @click="methodFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="methodFilterVisible ? '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="methodFilters" direction="vertical" size="small">
<a-checkbox v-for="key of RequestMethods" :key="key" :value="key">
<apiMethodName :method="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"
>
<MsButton type="text" class="arco-btn-text--secondary" @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 #num="{ record }">
<MsButton type="text" @click="openApiDetail(record.id)">{{ record.num }}</MsButton>
</template>
<template #method="{ record }">
<apiMethodName :method="record.method" tag-size="small" is-tag />
</template>
<template #status="{ record }">
<apiStatus :status="record.status" size="small" />
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { RouteRecordName } from 'vue-router';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { getCasePage, getDefinitionPage } from '@/api/modules/api-test/management';
import { getScenarioPage } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{
type: 'api' | 'case' | 'scenario';
module: MsTreeNodeData;
protocol: string;
projectId: string | number;
moduleIds: (string | number)[]; // id id
}>();
const emit = defineEmits<{
(e: 'select', ids: (string | number)[]): void;
}>();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
const keyword = ref('');
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 100,
},
{
title: 'apiTestManagement.apiName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
},
{
title: 'apiTestManagement.apiType',
dataIndex: 'method',
slotName: 'method',
titleSlotName: 'methodFilter',
width: 140,
},
{
title: 'apiTestManagement.apiStatus',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
width: 130,
},
{
title: 'apiTestManagement.path',
dataIndex: 'path',
showTooltip: true,
width: 200,
},
{
title: 'apiTestManagement.version',
dataIndex: 'versionName',
width: 100,
},
{
title: 'common.tag',
dataIndex: 'tags',
isTag: true,
isStringTag: true,
width: 150,
},
];
const tableConfig = {
columns,
scroll: { x: 700 },
selectable: true,
showSelectorAll: false,
heightUsed: 300,
};
//
const useApiTable = useTable(getDefinitionPage, tableConfig);
//
const useCaseTable = useTable(getCasePage, tableConfig);
//
const useScenarioTable = useTable(getScenarioPage, tableConfig);
const methodFilterVisible = ref(false);
const methodFilters = ref(Object.keys(RequestMethods));
const statusFilterVisible = ref(false);
const statusFilters = ref(Object.keys(RequestDefinitionStatus));
const tableSelected = ref<(string | number)[]>([]);
//
const currentTable = computed(() => {
switch (props.type) {
case 'api':
return useApiTable;
case 'case':
return useCaseTable;
case 'scenario':
default:
return useScenarioTable;
}
});
function loadPage(ids?: (string | number)[]) {
nextTick(() => {
// currentTable
currentTable.value.setLoadListParams({
keyword: keyword.value,
projectId: props.projectId,
moduleIds: ids || props.moduleIds,
protocol: props.protocol,
filter: {
status:
statusFilters.value.length === Object.keys(RequestDefinitionStatus).length
? undefined
: statusFilters.value,
method: methodFilters.value.length === Object.keys(RequestMethods).length ? undefined : methodFilters.value,
},
});
currentTable.value.loadList();
});
}
function handleFilterHidden(val: boolean) {
if (!val) {
loadPage();
}
}
function resetTable() {
currentTable.value.resetSelector();
keyword.value = '';
methodFilters.value = Object.keys(RequestMethods);
statusFilters.value = Object.keys(RequestDefinitionStatus);
loadPage();
}
/**
* 处理表格选中
*/
function handleTableSelect(arr: (string | number)[]) {
tableSelected.value = arr;
emit('select', arr);
}
function openApiDetail(id: string | number) {
let routeName: RouteRecordName;
const query: Record<string, any> = {};
switch (props.type) {
case 'api':
routeName = ApiTestRouteEnum.API_TEST_MANAGEMENT;
query.dId = id;
break;
case 'case':
routeName = ApiTestRouteEnum.API_TEST_MANAGEMENT;
query.cId = id;
break;
case 'scenario':
default:
routeName = ApiTestRouteEnum.API_TEST_SCENARIO;
break;
}
openNewPage(routeName, query);
}
defineExpose({
resetTable,
loadPage,
});
</script>
<style lang="less" scoped></style>

View File

@ -123,7 +123,7 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import executeStatus from '../common/executeStatus.vue';
import importApiDrawer from '../common/importApiDrawer.vue';
import importApiDrawer from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType.vue';
import { useI18n } from '@/hooks/useI18n';

View File

@ -124,4 +124,9 @@ export default {
'api_scenario.recycle.recover': '恢复',
'api_scenario.recycle.list': '回收站列表',
'api_scenario.recycle.batchCleanOut': '彻底删除',
'apiScenario.quoteTreeNoData': '暂无可引用数据,可切换项目获取数据',
'apiScenario.quoteTreeSearchTip': '输入模块名称搜索',
'apiScenario.quoteTableSearchTip': '通过路径或名称搜索',
'apiScenario.collapseAll': '收起全部子模块',
'apiScenario.expandAll': '展开全部子模块',
};