feat(接口场景): 场景步骤 3%&部分 bug 解决
This commit is contained in:
parent
ffbe77bb73
commit
5e17455904
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,4 +1,5 @@
|
|||
<template>
|
||||
<div v-if="props.method">
|
||||
<MsTag
|
||||
v-if="props.isTag"
|
||||
:self-style="{
|
||||
|
@ -11,6 +12,8 @@
|
|||
{{ 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));
|
||||
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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -150,6 +150,7 @@
|
|||
|
||||
const initDefaultId = `debug-${Date.now()}`;
|
||||
const defaultDebugParams: RequestParam = {
|
||||
type: 'api',
|
||||
id: initDefaultId,
|
||||
moduleId: 'root',
|
||||
protocol: 'HTTP',
|
||||
|
|
|
@ -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, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
||||
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(() => {
|
||||
|
|
|
@ -409,14 +409,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
watch(
|
||||
() => props.detail.id,
|
||||
() => {
|
||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
[activeResponse.value] = previewDetail.value.responseDefinition || [];
|
||||
if (previewDetail.value.protocol !== 'HTTP') {
|
||||
// 初始化插件脚本
|
||||
initPluginScript(previewDetail.value.protocol);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const activeDetailKey = ref(['request', 'response']);
|
||||
|
||||
|
|
|
@ -109,7 +109,9 @@
|
|||
|
||||
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
|
||||
|
||||
watchEffect(() => {
|
||||
watch(
|
||||
() => props.detail.id,
|
||||
() => {
|
||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
if (props.isCaseDetail) return;
|
||||
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
||||
|
@ -129,7 +131,11 @@
|
|||
query: tableParam.query,
|
||||
responseDefinition: tableParam.response,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const description = computed(() => {
|
||||
const commonDescription = [
|
||||
|
|
|
@ -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[]; // 模块树
|
||||
}>();
|
||||
|
|
|
@ -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: [], // 后置依赖
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>>({});
|
||||
|
|
|
@ -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 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';
|
||||
|
|
|
@ -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': '展开全部子模块',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue