feat(接口场景): 场景步骤 3%&部分 bug 解决
This commit is contained in:
parent
ffbe77bb73
commit
5e17455904
|
@ -23,6 +23,7 @@
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:project', val: string): void;
|
(e: 'update:project', val: string): void;
|
||||||
|
(e: 'change', val: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
|
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
|
||||||
) {
|
) {
|
||||||
emit('update:project', value as string);
|
emit('update:project', value as string);
|
||||||
|
emit('change', value as string);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
>
|
>
|
||||||
<icon-drag-dot-vertical class="absolute left-[-3px] top-[50%] w-[14px]" size="14" />
|
<icon-drag-dot-vertical class="absolute left-[-3px] top-[50%] w-[14px]" size="14" />
|
||||||
</div>
|
</div>
|
||||||
<a-scrollbar class="h-full overflow-y-auto">
|
<a-scrollbar class="ms-drawer-body-scrollbar">
|
||||||
<div class="ms-drawer-body">
|
<div class="ms-drawer-body">
|
||||||
<slot>
|
<slot>
|
||||||
<MsDescription
|
<MsDescription
|
||||||
|
@ -294,6 +294,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-drawer {
|
.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 {
|
.ms-drawer-body {
|
||||||
@apply h-full;
|
@apply h-full;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,16 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<MsTag
|
<div v-if="props.method">
|
||||||
v-if="props.isTag"
|
<MsTag
|
||||||
:self-style="{
|
v-if="props.isTag"
|
||||||
border: `1px solid ${props.tagBackgroundColor || methodColor}`,
|
:self-style="{
|
||||||
color: props.tagTextColor || methodColor,
|
border: `1px solid ${props.tagBackgroundColor || methodColor}`,
|
||||||
backgroundColor: props.tagBackgroundColor || 'white',
|
color: props.tagTextColor || methodColor,
|
||||||
}"
|
backgroundColor: props.tagBackgroundColor || 'white',
|
||||||
:size="props.tagSize"
|
}"
|
||||||
>
|
:size="props.tagSize"
|
||||||
{{ props.method }}
|
>
|
||||||
</MsTag>
|
{{ props.method }}
|
||||||
<div v-else class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
|
</MsTag>
|
||||||
|
<div v-else class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>-</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -20,7 +23,7 @@
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
method: RequestMethods;
|
method?: RequestMethods;
|
||||||
isTag?: boolean;
|
isTag?: boolean;
|
||||||
tagSize?: Size;
|
tagSize?: Size;
|
||||||
tagBackgroundColor?: string;
|
tagBackgroundColor?: string;
|
||||||
|
@ -60,8 +63,11 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
const methodColor = computed(() => {
|
const methodColor = computed(() => {
|
||||||
const colorMap = colorMaps.find((item) => item.includes.includes(props.method));
|
if (props.method) {
|
||||||
return colorMap?.color || 'rgb(var(--link-7))'; // 方法映射内找不到对应的 key 说明是插件,所有的插件协议颜色都是一样的
|
const colorMap = colorMaps.find((item) => item.includes.includes(props.method!));
|
||||||
|
return colorMap?.color || 'rgb(var(--link-7))'; // 方法映射内找不到对应的 key 说明是插件,所有的插件协议颜色都是一样的
|
||||||
|
}
|
||||||
|
return 'rgb(var(--link-7))';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -144,8 +144,13 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleFileChange() {
|
async function handleFileChange(files: MsFileItem[]) {
|
||||||
if (!props.uploadTempFileApi) return;
|
if (!props.uploadTempFileApi) return;
|
||||||
|
if (files.length === 0) {
|
||||||
|
innerParams.value.binaryBody.file = undefined;
|
||||||
|
emit('change');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (fileList.value[0]?.local && fileList.value[0].file) {
|
if (fileList.value[0]?.local && fileList.value[0].file) {
|
||||||
appStore.showLoading();
|
appStore.showLoading();
|
||||||
|
|
|
@ -563,10 +563,11 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface RequestCustomAttr {
|
export interface RequestCustomAttr {
|
||||||
|
type: 'api' | 'case' | 'mock' | 'doc'; // 展示的请求 tab 类型;api包含了接口调试和接口定义
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
activeTab: RequestComposition;
|
activeTab: RequestComposition;
|
||||||
mode?: 'definition' | 'debug' | 'case';
|
mode?: 'definition' | 'debug'; // 接口定义时,展示的定义模式/调试模式(显示的 tab 不同)
|
||||||
executeLoading: boolean; // 执行中loading
|
executeLoading: boolean; // 执行中loading
|
||||||
isCopy?: boolean; // 是否是复制
|
isCopy?: boolean; // 是否是复制
|
||||||
isExecute?: boolean; // 是否是执行
|
isExecute?: boolean; // 是否是执行
|
||||||
|
@ -673,6 +674,7 @@
|
||||||
];
|
];
|
||||||
// 根据协议类型获取请求内容tab
|
// 根据协议类型获取请求内容tab
|
||||||
const contentTabList = computed(() => {
|
const contentTabList = computed(() => {
|
||||||
|
// HTTP 协议 tabs
|
||||||
if (isHttpProtocol.value) {
|
if (isHttpProtocol.value) {
|
||||||
if (props.isDefinition) {
|
if (props.isDefinition) {
|
||||||
// 接口定义,定义模式隐藏前后置、断言
|
// 接口定义,定义模式隐藏前后置、断言
|
||||||
|
@ -683,6 +685,7 @@
|
||||||
// 接口调试无断言
|
// 接口调试无断言
|
||||||
return httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
|
return httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
|
||||||
}
|
}
|
||||||
|
// 插件 tabs
|
||||||
if (props.isDefinition) {
|
if (props.isDefinition) {
|
||||||
// 接口定义,定义模式隐藏前后置、断言
|
// 接口定义,定义模式隐藏前后置、断言
|
||||||
return requestVModel.value.mode === 'definition'
|
return requestVModel.value.mode === 'definition'
|
||||||
|
@ -1203,7 +1206,7 @@
|
||||||
await initProtocolList();
|
await initProtocolList();
|
||||||
}
|
}
|
||||||
await initPluginScript();
|
await initPluginScript();
|
||||||
} else {
|
} else if (protocolOptions.value.length === 0) {
|
||||||
await initProtocolList();
|
await initProtocolList();
|
||||||
}
|
}
|
||||||
if (props.request.isExecute && !requestVModel.value.executeLoading) {
|
if (props.request.isExecute && !requestVModel.value.executeLoading) {
|
||||||
|
|
|
@ -363,8 +363,13 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleFileChange() {
|
async function handleFileChange(files: MsFileItem[]) {
|
||||||
if (!props.uploadTempFileApi) return;
|
if (!props.uploadTempFileApi) return;
|
||||||
|
if (files.length === 0) {
|
||||||
|
activeResponse.value.binaryBody.file = undefined;
|
||||||
|
emit('change');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (fileList.value[0]?.local && fileList.value[0].file) {
|
if (fileList.value[0]?.local && fileList.value[0].file) {
|
||||||
appStore.showLoading();
|
appStore.showLoading();
|
||||||
|
|
|
@ -150,6 +150,7 @@
|
||||||
|
|
||||||
const initDefaultId = `debug-${Date.now()}`;
|
const initDefaultId = `debug-${Date.now()}`;
|
||||||
const defaultDebugParams: RequestParam = {
|
const defaultDebugParams: RequestParam = {
|
||||||
|
type: 'api',
|
||||||
id: initDefaultId,
|
id: initDefaultId,
|
||||||
moduleId: 'root',
|
moduleId: 'root',
|
||||||
protocol: 'HTTP',
|
protocol: 'HTTP',
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
|
<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
|
<a-tab-pane
|
||||||
v-if="!activeApiTab.isNew"
|
v-if="!activeApiTab.isNew"
|
||||||
key="preview"
|
key="preview"
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
class="ms-api-tab-pane"
|
class="ms-api-tab-pane"
|
||||||
>
|
>
|
||||||
<preview
|
<preview
|
||||||
v-if="definitionActiveKey === 'preview'"
|
v-if="activeApiTab.definitionActiveKey === 'preview'"
|
||||||
:detail="activeApiTab"
|
:detail="activeApiTab"
|
||||||
:module-tree="props.moduleTree"
|
:module-tree="props.moduleTree"
|
||||||
:protocols="protocols"
|
:protocols="protocols"
|
||||||
|
@ -116,7 +116,6 @@
|
||||||
|
|
||||||
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||||
|
|
||||||
const definitionActiveKey = ref('definition');
|
|
||||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -145,6 +144,8 @@
|
||||||
|
|
||||||
const initDefaultId = `definition-${Date.now()}`;
|
const initDefaultId = `definition-${Date.now()}`;
|
||||||
const defaultDefinitionParams: RequestParam = {
|
const defaultDefinitionParams: RequestParam = {
|
||||||
|
type: 'api',
|
||||||
|
definitionActiveKey: 'definition',
|
||||||
id: initDefaultId,
|
id: initDefaultId,
|
||||||
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
||||||
protocol: 'HTTP',
|
protocol: 'HTTP',
|
||||||
|
@ -220,12 +221,10 @@
|
||||||
label: t('apiTestManagement.newApi'),
|
label: t('apiTestManagement.newApi'),
|
||||||
id,
|
id,
|
||||||
isNew: !defaultProps?.id, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
isNew: !defaultProps?.id, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
||||||
|
definitionActiveKey: !defaultProps ? 'definition' : 'preview',
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
});
|
});
|
||||||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||||
if (!defaultProps) {
|
|
||||||
definitionActiveKey.value = 'definition';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiTableRef = ref<InstanceType<typeof apiTable>>();
|
const apiTableRef = ref<InstanceType<typeof apiTable>>();
|
||||||
|
@ -257,7 +256,6 @@
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
|
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
|
||||||
const name = isCopy ? `copy-${res.name}` : res.name;
|
const name = isCopy ? `copy-${res.name}` : res.name;
|
||||||
definitionActiveKey.value = isCopy || isExecute ? 'definition' : 'preview';
|
|
||||||
let parseRequestBodyResult;
|
let parseRequestBodyResult;
|
||||||
if (res.protocol === 'HTTP') {
|
if (res.protocol === 'HTTP') {
|
||||||
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||||
|
@ -276,6 +274,7 @@
|
||||||
id: isCopy ? new Date().getTime() : res.id,
|
id: isCopy ? new Date().getTime() : res.id,
|
||||||
isExecute,
|
isExecute,
|
||||||
mode: isExecute ? 'debug' : 'definition',
|
mode: isExecute ? 'debug' : 'definition',
|
||||||
|
definitionActiveKey: isCopy || isExecute ? 'definition' : 'preview',
|
||||||
...parseRequestBodyResult,
|
...parseRequestBodyResult,
|
||||||
});
|
});
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|
|
@ -409,14 +409,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watch(
|
||||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
() => props.detail.id,
|
||||||
[activeResponse.value] = previewDetail.value.responseDefinition || [];
|
() => {
|
||||||
if (previewDetail.value.protocol !== 'HTTP') {
|
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||||
// 初始化插件脚本
|
[activeResponse.value] = previewDetail.value.responseDefinition || [];
|
||||||
initPluginScript(previewDetail.value.protocol);
|
if (previewDetail.value.protocol !== 'HTTP') {
|
||||||
|
// 初始化插件脚本
|
||||||
|
initPluginScript(previewDetail.value.protocol);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const activeDetailKey = ref(['request', 'response']);
|
const activeDetailKey = ref(['request', 'response']);
|
||||||
|
|
||||||
|
|
|
@ -109,27 +109,33 @@
|
||||||
|
|
||||||
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
|
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
|
||||||
|
|
||||||
watchEffect(() => {
|
watch(
|
||||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
() => props.detail.id,
|
||||||
if (props.isCaseDetail) return;
|
() => {
|
||||||
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||||
previewDetail.value = {
|
if (props.isCaseDetail) return;
|
||||||
...previewDetail.value,
|
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
||||||
body: {
|
previewDetail.value = {
|
||||||
...previewDetail.value.body,
|
...previewDetail.value,
|
||||||
formDataBody: {
|
body: {
|
||||||
formValues: tableParam.formDataBodyTableParams,
|
...previewDetail.value.body,
|
||||||
|
formDataBody: {
|
||||||
|
formValues: tableParam.formDataBodyTableParams,
|
||||||
|
},
|
||||||
|
wwwFormBody: {
|
||||||
|
formValues: tableParam.wwwFormBodyTableParams,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
wwwFormBody: {
|
headers: tableParam.headers,
|
||||||
formValues: tableParam.wwwFormBodyTableParams,
|
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 description = computed(() => {
|
||||||
const commonDescription = [
|
const commonDescription = [
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { getProtocolList } from '@/api/modules/api-test/common';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
|
@ -17,8 +19,6 @@
|
||||||
|
|
||||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
|
|
||||||
const preview = defineAsyncComponent(() => import('../api/preview/index.vue'));
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
moduleTree: ModuleTreeNode[]; // 模块树
|
moduleTree: ModuleTreeNode[]; // 模块树
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@open-case-tab="openCaseTab"
|
@open-case-tab="openCaseTab"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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" />
|
<caseDetail :active-api-tab="activeApiTab" :module-tree="props.moduleTree" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,6 @@
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||||
import caseDetail from './caseDetail.vue';
|
|
||||||
import caseTable from './caseTable.vue';
|
import caseTable from './caseTable.vue';
|
||||||
|
|
||||||
import { getCaseDetail } from '@/api/modules/api-test/management';
|
import { getCaseDetail } from '@/api/modules/api-test/management';
|
||||||
|
@ -31,6 +30,9 @@
|
||||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
|
// 非首屏渲染的大量内容的组件异步导入
|
||||||
|
const caseDetail = defineAsyncComponent(() => import('./caseDetail.vue'));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
|
|
||||||
const initDefaultId = `case-${Date.now()}`;
|
const initDefaultId = `case-${Date.now()}`;
|
||||||
const defaultCaseParams: RequestParam = {
|
const defaultCaseParams: RequestParam = {
|
||||||
|
type: 'case',
|
||||||
id: initDefaultId,
|
id: initDefaultId,
|
||||||
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
||||||
protocol: 'HTTP',
|
protocol: 'HTTP',
|
||||||
|
@ -105,7 +108,7 @@
|
||||||
response: cloneDeep(defaultResponse),
|
response: cloneDeep(defaultResponse),
|
||||||
responseDefinition: [cloneDeep(defaultResponseItem)],
|
responseDefinition: [cloneDeep(defaultResponseItem)],
|
||||||
isNew: true,
|
isNew: true,
|
||||||
mode: 'case',
|
unSaved: false,
|
||||||
executeLoading: false,
|
executeLoading: false,
|
||||||
preDependency: [], // 前置依赖
|
preDependency: [], // 前置依赖
|
||||||
postDependency: [], // 后置依赖
|
postDependency: [], // 后置依赖
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<api
|
<api
|
||||||
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.mode === 'definition'"
|
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.type === 'api'"
|
||||||
ref="apiRef"
|
ref="apiRef"
|
||||||
v-model:active-api-tab="activeApiTab"
|
v-model:active-api-tab="activeApiTab"
|
||||||
v-model:api-tabs="apiTabs"
|
v-model:api-tabs="apiTabs"
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
:module-tree="props.moduleTree"
|
:module-tree="props.moduleTree"
|
||||||
/>
|
/>
|
||||||
<apiCase
|
<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:api-tabs="apiTabs"
|
||||||
v-model:active-api-tab="activeApiTab"
|
v-model:active-api-tab="activeApiTab"
|
||||||
:active-module="props.activeModule"
|
:active-module="props.activeModule"
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
watch(
|
watch(
|
||||||
() => activeApiTab.value.id,
|
() => activeApiTab.value.id,
|
||||||
() => {
|
() => {
|
||||||
if (typeof setActiveApi === 'function' && !activeApiTab.value.isNew) {
|
if (typeof setActiveApi === 'function' && !activeApiTab.value.isNew && activeApiTab.value.type === 'api') {
|
||||||
// 打开的 tab 是接口详情的 tab 才需要同步设置模块树的激活节点
|
// 打开的 tab 是接口详情的 tab 才需要同步设置模块树的激活节点
|
||||||
setActiveApi(activeApiTab.value);
|
setActiveApi(activeApiTab.value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide } from 'vue';
|
import { provide } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
|
@ -38,12 +37,8 @@
|
||||||
import moduleTree from './components/moduleTree.vue';
|
import moduleTree from './components/moduleTree.vue';
|
||||||
import management from './components/recycle/index.vue';
|
import management from './components/recycle/index.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
|
||||||
|
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
const activeModule = ref<string>('all');
|
const activeModule = ref<string>('all');
|
||||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
const folderTreePathMap = ref<Record<string, any>>({});
|
const folderTreePathMap = ref<Record<string, any>>({});
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -123,7 +123,7 @@
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
import executeStatus from '../common/executeStatus.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 stepType from '../common/stepType.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
|
@ -124,4 +124,9 @@ export default {
|
||||||
'api_scenario.recycle.recover': '恢复',
|
'api_scenario.recycle.recover': '恢复',
|
||||||
'api_scenario.recycle.list': '回收站列表',
|
'api_scenario.recycle.list': '回收站列表',
|
||||||
'api_scenario.recycle.batchCleanOut': '彻底删除',
|
'api_scenario.recycle.batchCleanOut': '彻底删除',
|
||||||
|
'apiScenario.quoteTreeNoData': '暂无可引用数据,可切换项目获取数据',
|
||||||
|
'apiScenario.quoteTreeSearchTip': '输入模块名称搜索',
|
||||||
|
'apiScenario.quoteTableSearchTip': '通过路径或名称搜索',
|
||||||
|
'apiScenario.collapseAll': '收起全部子模块',
|
||||||
|
'apiScenario.expandAll': '展开全部子模块',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue