feat(接口管理): 接口定义-导入&前后置数据源切换

This commit is contained in:
baiqi 2024-03-08 17:02:44 +08:00 committed by 刘瑞斌
parent 0c8103011b
commit 4332f78df3
47 changed files with 1554 additions and 812 deletions

View File

@ -142,7 +142,7 @@
"stylelint-less": "^1.0.8", "stylelint-less": "^1.0.8",
"stylelint-order": "^5.0.0", "stylelint-order": "^5.0.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^4.9.5", "typescript": "^5.4.2",
"unplugin-auto-import": "^0.16.7", "unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.24.1", "unplugin-vue-components": "^0.24.1",
"vite": "^3.2.7", "vite": "^3.2.7",

View File

@ -1,7 +1,21 @@
import MSR from '@/api/http/index'; import MSR from '@/api/http/index';
import { GetPluginOptionsUrl, GetPluginScriptUrl, GetProtocolListUrl } from '@/api/requrls/api-test/common'; import {
GetEnvironmentUrl,
GetEnvListUrl,
GetPluginOptionsUrl,
GetPluginScriptUrl,
GetProtocolListUrl,
LocalExecuteApiDebugUrl,
} from '@/api/requrls/api-test/common';
import { GetPluginOptionsParams, PluginConfig, PluginOption, ProtocolItem } from '@/models/apiTest/common'; import {
ExecuteRequestParams,
GetPluginOptionsParams,
PluginConfig,
PluginOption,
ProtocolItem,
} from '@/models/apiTest/common';
import { EnvConfig, EnvironmentItem } from '@/models/projectManagement/environmental';
// 获取协议列表 // 获取协议列表
export function getProtocolList(organizationId: string) { export function getProtocolList(organizationId: string) {
@ -17,3 +31,18 @@ export function getPluginOptions(data: GetPluginOptionsParams) {
export function getPluginScript(pluginId: string) { export function getPluginScript(pluginId: string) {
return MSR.get<PluginConfig>({ url: GetPluginScriptUrl, params: pluginId }); return MSR.get<PluginConfig>({ url: GetPluginScriptUrl, params: pluginId });
} }
// 本地执行调试
export function localExecuteApiDebug(host: string, data: ExecuteRequestParams) {
return MSR.post<ExecuteRequestParams>({ url: `${host}${LocalExecuteApiDebugUrl}`, data });
}
// 获取环境列表
export function getEnvList(projectId: string) {
return MSR.get<EnvironmentItem[]>({ url: GetEnvListUrl, params: projectId });
}
// 获取环境详情
export function getEnvironment(envId: string) {
return MSR.get<EnvConfig>({ url: GetEnvironmentUrl, params: envId });
}

View File

@ -9,7 +9,6 @@ import {
GetApiDebugDetailUrl, GetApiDebugDetailUrl,
GetDebugModuleCountUrl, GetDebugModuleCountUrl,
GetDebugModulesUrl, GetDebugModulesUrl,
LocalExecuteApiDebugUrl,
MoveDebugModuleUrl, MoveDebugModuleUrl,
TestMockUrl, TestMockUrl,
TransferFileUrl, TransferFileUrl,
@ -69,11 +68,6 @@ export function executeDebug(data: ExecuteRequestParams) {
return MSR.post<ExecuteRequestParams>({ url: ExecuteApiDebugUrl, data }); return MSR.post<ExecuteRequestParams>({ url: ExecuteApiDebugUrl, data });
} }
// 本地执行调试
export function localExecuteApiDebug(host: string, data: ExecuteRequestParams) {
return MSR.post<ExecuteRequestParams>({ url: `${host}${LocalExecuteApiDebugUrl}`, data });
}
// 新增调试 // 新增调试
export function addDebug(data: SaveDebugParams) { export function addDebug(data: SaveDebugParams) {
return MSR.post({ url: AddApiDebugUrl, data }); return MSR.post({ url: AddApiDebugUrl, data });

View File

@ -1,28 +1,43 @@
import MSR from '@/api/http/index'; import MSR from '@/api/http/index';
import { import {
AddDefinitionScheduleUrl,
AddDefinitionUrl, AddDefinitionUrl,
AddModuleUrl, AddModuleUrl,
BatchDeleteDefinitionUrl, BatchDeleteDefinitionUrl,
BatchMoveDefinitionUrl,
BatchUpdateDefinitionUrl,
CheckDefinitionScheduleUrl,
DebugDefinitionUrl,
DefinitionMockPageUrl, DefinitionMockPageUrl,
DefinitionPageUrl, DefinitionPageUrl,
DeleteDefinitionScheduleUrl,
DeleteDefinitionUrl, DeleteDefinitionUrl,
DeleteMockUrl, DeleteMockUrl,
DeleteModuleUrl, DeleteModuleUrl,
GetDefinitionDetailUrl, GetDefinitionDetailUrl,
GetDefinitionScheduleUrl,
GetEnvModuleUrl, GetEnvModuleUrl,
GetModuleCountUrl, GetModuleCountUrl,
GetModuleOnlyTreeUrl, GetModuleOnlyTreeUrl,
GetModuleTreeUrl, GetModuleTreeUrl,
ImportDefinitionUrl,
MoveModuleUrl, MoveModuleUrl,
SortDefinitionUrl,
SwitchDefinitionScheduleUrl,
TransferFileModuleOptionUrl, TransferFileModuleOptionUrl,
TransferFileUrl, TransferFileUrl,
UpdateDefinitionScheduleUrl,
UpdateDefinitionUrl, UpdateDefinitionUrl,
UpdateMockStatusUrl, UpdateMockStatusUrl,
UpdateModuleUrl, UpdateModuleUrl,
UploadTempFileUrl, UploadTempFileUrl,
} from '@/api/requrls/api-test/management'; } from '@/api/requrls/api-test/management';
import { ExecuteRequestParams } from '@/models/apiTest/common';
import { import {
ApiDefinitionBatchDeleteParams,
ApiDefinitionBatchMoveParams,
ApiDefinitionBatchUpdateParams,
ApiDefinitionCreateParams, ApiDefinitionCreateParams,
ApiDefinitionDetail, ApiDefinitionDetail,
ApiDefinitionGetEnvModuleParams, ApiDefinitionGetEnvModuleParams,
@ -32,10 +47,21 @@ import {
ApiDefinitionPageParams, ApiDefinitionPageParams,
ApiDefinitionUpdateModuleParams, ApiDefinitionUpdateModuleParams,
ApiDefinitionUpdateParams, ApiDefinitionUpdateParams,
CheckScheduleParams,
CreateImportApiDefinitionScheduleParams,
EnvModule, EnvModule,
ImportApiDefinitionParams,
mockParams, mockParams,
UpdateScheduleParams,
} from '@/models/apiTest/management'; } from '@/models/apiTest/management';
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common'; import {
AddModuleParams,
CommonList,
DragSortParams,
ModuleTreeNode,
MoveModules,
TransferFileParams,
} from '@/models/common';
// 更新模块 // 更新模块
export function updateModule(data: ApiDefinitionUpdateModuleParams) { export function updateModule(data: ApiDefinitionUpdateModuleParams) {
@ -94,7 +120,7 @@ export function updateDefinition(data: ApiDefinitionUpdateParams) {
// 获取接口定义详情 // 获取接口定义详情
export function getDefinitionDetail(id: string) { export function getDefinitionDetail(id: string) {
return MSR.get({ url: GetDefinitionDetailUrl, params: id }); return MSR.get<ApiDefinitionDetail>({ url: GetDefinitionDetailUrl, params: id });
} }
// 文件转存 // 文件转存
@ -118,8 +144,63 @@ export function deleteDefinition(id: string) {
} }
// 批量删除定义 // 批量删除定义
export function batchDeleteDefinition(id: string) { export function batchDeleteDefinition(data: ApiDefinitionBatchDeleteParams) {
return MSR.get({ url: BatchDeleteDefinitionUrl, params: id }); return MSR.post({ url: BatchDeleteDefinitionUrl, data });
}
// 导入定义
export function importDefinition(params: ImportApiDefinitionParams) {
return MSR.uploadFile({ url: ImportDefinitionUrl }, { fileList: [params.file], request: params.request }, 'file');
}
// 拖拽定义节点
export function sortDefinition(data: DragSortParams) {
return MSR.post({ url: SortDefinitionUrl, data });
}
// 批量更新定义
export function batchUpdateDefinition(data: ApiDefinitionBatchUpdateParams) {
return MSR.post({ url: BatchUpdateDefinitionUrl, data });
}
// 批量移动定义
export function batchMoveDefinition(data: ApiDefinitionBatchMoveParams) {
return MSR.post({ url: BatchMoveDefinitionUrl, data });
}
// 更新定时同步
export function updateDefinitionSchedule(data: UpdateScheduleParams) {
return MSR.post({ url: UpdateDefinitionScheduleUrl, data });
}
// 定时同步-检查 url 是否存在
export function checkDefinitionSchedule(data: CheckScheduleParams) {
return MSR.post({ url: CheckDefinitionScheduleUrl, data });
}
// 添加定时同步
export function createDefinitionSchedule(data: CreateImportApiDefinitionScheduleParams) {
return MSR.post({ url: AddDefinitionScheduleUrl, data });
}
// 定时同步-开启关闭
export function switchDefinitionSchedule(id: string) {
return MSR.get({ url: SwitchDefinitionScheduleUrl, params: id });
}
// 查询定时同步详情
export function getDefinitionSchedule(id: string) {
return MSR.get({ url: GetDefinitionScheduleUrl, params: id });
}
// 删除定时同步
export function deleteDefinitionSchedule(id: string) {
return MSR.get({ url: DeleteDefinitionScheduleUrl, params: id });
}
// 接口定义调试
export function debugDefinition(data: ExecuteRequestParams) {
return MSR.post({ url: DebugDefinitionUrl, data });
} }
/** /**

View File

@ -1,3 +1,6 @@
export const GetProtocolListUrl = '/api/test/protocol'; // 获取协议列表 export const GetProtocolListUrl = '/api/test/protocol'; // 获取协议列表
export const GetPluginOptionsUrl = '/api/test/plugin/form/option'; // 获取插件表单选项 export const GetPluginOptionsUrl = '/api/test/plugin/form/option'; // 获取插件表单选项
export const GetPluginScriptUrl = '/api/test/plugin/script'; // 获取插件配置脚本 export const GetPluginScriptUrl = '/api/test/plugin/script'; // 获取插件配置脚本
export const LocalExecuteApiDebugUrl = '/api/debug'; // 本地执行调试
export const GetEnvListUrl = '/api/test/env-list'; // 获取接口测试环境列表
export const GetEnvironmentUrl = '/api/test/environment'; // 获取接口测试环境详情

View File

@ -1,5 +1,4 @@
export const ExecuteApiDebugUrl = '/api/debug/debug'; // 执行调试 export const ExecuteApiDebugUrl = '/api/debug/debug'; // 执行调试
export const LocalExecuteApiDebugUrl = '/api/debug'; // 本地执行调试
export const AddApiDebugUrl = '/api/debug/add'; // 新增调试 export const AddApiDebugUrl = '/api/debug/add'; // 新增调试
export const UpdateApiDebugUrl = '/api/debug/update'; // 更新调试 export const UpdateApiDebugUrl = '/api/debug/update'; // 更新调试
export const GetApiDebugDetailUrl = '/api/debug/get'; // 获取接口调试详情 export const GetApiDebugDetailUrl = '/api/debug/get'; // 获取接口调试详情

View File

@ -13,8 +13,20 @@ export const GetDefinitionDetailUrl = '/api/definition/get-detail'; // 获取接
export const TransferFileUrl = '/api/definition/transfer'; // 文件转存 export const TransferFileUrl = '/api/definition/transfer'; // 文件转存
export const TransferFileModuleOptionUrl = '/api/definition/transfer/options'; // 文件转存目录 export const TransferFileModuleOptionUrl = '/api/definition/transfer/options'; // 文件转存目录
export const UploadTempFileUrl = '/api/definition/upload/temp/file'; // 临时文件上传 export const UploadTempFileUrl = '/api/definition/upload/temp/file'; // 临时文件上传
export const DeleteDefinitionUrl = '/api/definition/delete'; // 删除接口定义
export const BatchDeleteDefinitionUrl = '/api/definition/batch-del'; // 批量删除接口定义
export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表 export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表
export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态 export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态
export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock
export const DeleteDefinitionUrl = '/api/definition/delete-to-gc'; // 删除接口定义
export const ImportDefinitionUrl = '/api/definition/import'; // 导入接口定义
export const SortDefinitionUrl = '/api/definition/edit/pos'; // 接口定义拖拽
export const CopyDefinitionUrl = '/api/definition/copy'; // 复制接口定义
export const BatchUpdateDefinitionUrl = '/api/definition/batch-update'; // 批量更新接口定义
export const BatchMoveDefinitionUrl = '/api/definition/batch-move'; // 批量移动接口定义
export const BatchDeleteDefinitionUrl = '/api/definition/batch/delete-to-gc'; // 批量删除接口定义
export const UpdateDefinitionScheduleUrl = '/api/definition/schedule/update'; // 接口定义-定时同步-更新
export const CheckDefinitionScheduleUrl = '/api/definition/schedule/check'; // 接口定义-定时同步-检查 url 是否存在
export const AddDefinitionScheduleUrl = '/api/definition/schedule/add'; // 接口定义-定时同步-添加
export const SwitchDefinitionScheduleUrl = '/api/definition/schedule/switch'; // 接口定义-定时同步-开启关闭
export const GetDefinitionScheduleUrl = '/api/definition/schedule/get'; // 接口定义-定时同步-查询
export const DeleteDefinitionScheduleUrl = '/api/definition/schedule/delete'; // 接口定义-定时同步-删除
export const DebugDefinitionUrl = '/api/definition/debug'; // 接口定义-调试

View File

@ -1,13 +0,0 @@
import _Comment from './comment';
import type { App } from 'vue';
const MsComment = Object.assign(_Comment, {
install: (app: App) => {
app.component(_Comment.name, _Comment);
},
});
export type CommentInstance = InstanceType<typeof _Comment>;
export { default as CommentInput } from './input.vue';
export default MsComment;

View File

@ -1,12 +0,0 @@
import EditComp from './edit-comp';
import type { App } from 'vue';
const MsEditComp = Object.assign(EditComp, {
install: (app: App) => {
app.component(EditComp.name, EditComp);
},
});
export type CommentInstance = InstanceType<typeof EditComp>;
export default MsEditComp;

View File

@ -160,7 +160,7 @@
import { APIKEY } from '@/models/user'; import { APIKEY } from '@/models/user';
const { copy } = useClipboard(); const { copy, isSupported } = useClipboard();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
@ -226,8 +226,12 @@
]; ];
async function handleCopy(val: string) { async function handleCopy(val: string) {
if (isSupported) {
await copy(val); await copy(val);
Message.success(t('ms.personal.copySuccess')); Message.success(t('ms.personal.copySuccess'));
} else {
Message.warning(t('common.copyNotSupport'));
}
} }
function desensitization(item: APIKEYItem) { function desensitization(item: APIKEYItem) {

View File

@ -24,6 +24,9 @@
<slot name="title" v-bind="_props"></slot> <slot name="title" v-bind="_props"></slot>
</a-tooltip> </a-tooltip>
</template> </template>
<template v-if="$slots['drag-icon']" #drag-icon="_props">
<slot name="title" v-bind="_props"></slot>
</template>
<template v-if="$slots['extra']" #extra="_props"> <template v-if="$slots['extra']" #extra="_props">
<div <div
v-if="_props.hideMoreAction !== true" v-if="_props.hideMoreAction !== true"
@ -298,6 +301,7 @@
dropNode: MsTreeNodeData; // dropNode: MsTreeNodeData; //
dropPosition: number; // -1 1 0 dropPosition: number; // -1 1 0
}) { }) {
console.log('dropNode', dropNode);
loop(originalTreeData.value, dragNode.key, (item, index, arr) => { loop(originalTreeData.value, dragNode.key, (item, index, arr) => {
arr.splice(index, 1); arr.splice(index, 1);
}); });

View File

@ -120,6 +120,9 @@
const { arrivedState } = useScroll(tabNav); const { arrivedState } = useScroll(tabNav);
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); //
/**
* 滚动tab
*/
const scrollTabs = (direction: 'left' | 'right') => { const scrollTabs = (direction: 'left' | 'right') => {
if (tabNav.value) { if (tabNav.value) {
const tabNavWidth = tabNav.value?.clientWidth || 0; const tabNavWidth = tabNav.value?.clientWidth || 0;
@ -139,6 +142,9 @@
} }
}; };
/**
* 滚动到当前激活的tab
*/
const scrollToActiveTab = () => { const scrollToActiveTab = () => {
const activeTabDom = tabNav.value?.querySelector('.ms-editable-tab.active'); const activeTabDom = tabNav.value?.querySelector('.ms-editable-tab.active');
if (activeTabDom) { if (activeTabDom) {
@ -169,6 +175,9 @@
return props.moreActionList ? [...dl, ...props.moreActionList] : dl; return props.moreActionList ? [...dl, ...props.moreActionList] : dl;
}); });
/**
* 监听激活的tab变化滚动到激活的tab
*/
watch( watch(
() => props.activeTab, () => props.activeTab,
() => { () => {
@ -192,14 +201,21 @@
emit('add'); emit('add');
} }
/**
* 关闭一个tab
*/
function closeOneTab(item: TabItem) { function closeOneTab(item: TabItem) {
const index = innerTabs.value.findIndex((e) => e.id === item.id); const index = innerTabs.value.findIndex((e) => e.id === item.id);
innerTabs.value.splice(index, 1); innerTabs.value.splice(index, 1);
if (innerActiveTab.value?.id === item.id && innerTabs.value[0]) { if (innerActiveTab.value?.id === item.id && innerTabs.value[0]) {
[innerActiveTab.value] = innerTabs.value; [innerActiveTab.value] = innerTabs.value;
emit('change', innerTabs.value[0]);
} }
} }
/**
* 关闭tab前处理
*/
function close(item: TabItem) { function close(item: TabItem) {
if (item.unSaved) { if (item.unSaved) {
openModal({ openModal({
@ -219,18 +235,24 @@
} }
function handleTabClick(item: TabItem) { function handleTabClick(item: TabItem) {
if (innerActiveTab.value?.id !== item.id) {
innerActiveTab.value = item; innerActiveTab.value = item;
nextTick(() => { nextTick(() => {
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}); });
emit('change', item); emit('change', item);
} }
}
/**
* 执行更多操作
*/
function executeAction(event: ActionsItem) { function executeAction(event: ActionsItem) {
switch (event.eventTag) { switch (event.eventTag) {
case 'closeAll': case 'closeAll':
innerTabs.value = innerTabs.value.filter((item) => item.closable === false); innerTabs.value = innerTabs.value.filter((item) => item.closable === false);
[innerActiveTab.value] = innerTabs.value; [innerActiveTab.value] = innerTabs.value;
emit('change', innerActiveTab.value);
break; break;
case 'closeOther': case 'closeOther':
innerTabs.value = innerTabs.value.filter( innerTabs.value = innerTabs.value.filter(
@ -243,6 +265,9 @@
} }
} }
/**
* 处理更多操作选择
*/
function handleMoreActionSelect(event: ActionsItem) { function handleMoreActionSelect(event: ActionsItem) {
if ( if (
(event.eventTag === 'closeAll' && innerTabs.value.some((item) => item.unSaved)) || (event.eventTag === 'closeAll' && innerTabs.value.some((item) => item.unSaved)) ||

View File

@ -0,0 +1,196 @@
<template>
<MsBaseTable
v-bind="propsRes"
:hoverable="false"
no-disable
is-simple-setting
:span-method="props.spanMethod"
v-on="propsEvent"
>
<template
v-for="item of props.columns.filter((e) => e.slotName !== undefined)"
#[item.slotName!]="{ record, rowIndex, column }"
>
<slot :name="item.slotName" v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }">
{{ record[item.dataIndex as string] || '-' }}
</slot>
</template>
</MsBaseTable>
</template>
<script setup lang="ts">
import { TableColumnData, TableData } from '@arco-design/web-vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumnData } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import useTableStore from '@/hooks/useTableStore';
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
import { TableOperationColumn } from '@arco-design/web-vue/es/table/interface';
export interface FormTableColumn extends MsTableColumnData {
enable?: boolean; //
[key: string]: any; //
}
const props = withDefaults(
defineProps<{
data?: any[];
columns: FormTableColumn[];
scroll?: {
x?: number | string;
y?: number | string;
maxHeight?: number | string;
minWidth?: number | string;
};
heightUsed?: number;
draggable?: boolean;
selectable?: boolean;
showSetting?: boolean; //
tableKey?: TableKeyEnum; // key showSettingtrue
disabled?: boolean; //
showSelectorAll?: boolean; //
isTreeTable?: boolean; //
spanMethod?: (data: {
record: TableData;
column: TableColumnData | TableOperationColumn;
rowIndex: number;
columnIndex: number;
}) => { rowspan?: number; colspan?: number } | void;
}>(),
{
data: () => [],
selectable: true,
showSetting: false,
tableKey: undefined,
}
);
const emit = defineEmits<{
(e: 'change', data: any[]): void; //
}>();
const tableStore = useTableStore();
async function initColumns() {
if (props.showSetting && props.tableKey) {
await tableStore.initColumn(props.tableKey, props.columns);
}
}
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
firstColumnWidth: 32,
tableKey: props.showSetting ? props.tableKey : undefined,
scroll: props.scroll,
heightUsed: props.heightUsed,
columns: props.columns,
selectable: props.selectable,
draggable: props.draggable ? { type: 'handle', width: 24 } : undefined,
showSetting: props.showSetting,
disabled: props.disabled,
showSelectorAll: props.showSelectorAll,
showPagination: false,
});
const selectedKeys = computed(() => propsRes.value.data.filter((e) => e.enable).map((e) => e.id));
propsEvent.value.rowSelectChange = (key: string) => {
propsRes.value.data = propsRes.value.data.map((e) => {
if (e.id === key) {
e.enable = !e.enable;
}
return e;
});
emit('change', propsRes.value.data);
};
propsEvent.value.selectAllChange = (v: SelectAllEnum) => {
propsRes.value.data = propsRes.value.data.map((e) => {
e.enable = v !== SelectAllEnum.NONE;
return e;
});
emit('change', propsRes.value.data);
};
watch(
() => selectedKeys.value,
(arr) => {
propsRes.value.selectedKeys = new Set(arr);
}
);
watch(
() => props.heightUsed,
(val) => {
propsRes.value.heightUsed = val;
}
);
watch(
() => props.data,
(val) => {
propsRes.value.data = val;
},
{
immediate: true,
}
);
await initColumns();
</script>
<style lang="less" scoped>
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
line-height: normal;
}
:deep(.arco-table .arco-table-cell) {
padding: 8px 2px;
}
:deep(.arco-table-cell-align-left) {
padding: 8px;
}
:deep(.arco-table-col-fixed-right) {
.arco-table-cell-align-left {
padding: 8px;
}
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
border-color: transparent !important;
.arco-input::placeholder {
@apply invisible;
}
.arco-select-view-icon {
@apply invisible;
}
.arco-select-view-value {
color: var(--color-text-1);
}
.arco-select {
border-color: transparent !important;
}
}
}
:deep(.param-input-number) {
@apply pr-0;
.arco-input {
@apply text-right;
}
.arco-input-suffix {
@apply hidden;
}
&:hover,
&.arco-input-focus {
.arco-input {
@apply text-left;
}
.arco-input-suffix {
@apply inline-flex;
}
}
}
:deep(.arco-table-expand-btn) {
background: transparent;
}
</style>

View File

@ -0,0 +1,18 @@
<template>
<MsFormTable :data="data" :columns="props.columns"> </MsFormTable>
</template>
<script setup lang="ts">
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
const props = defineProps<{
columns: FormTableColumn[];
}>();
const data = defineModel<Record<string, any>[]>('data', {
required: true,
default: () => [],
});
</script>
<style lang="less" scoped></style>

View File

@ -69,12 +69,14 @@
const { t } = useI18n(); const { t } = useI18n();
const innerModelValue = ref(props.modelValue); const innerModelValue = ref(props.modelValue);
const innerInputValue = ref(props.inputValue); const innerInputValue = defineModel<string>('inputValue', {
default: '',
});
const tagsLength = ref(0); // tagstag const tagsLength = ref(0); // tagstag
const isError = computed( const isError = computed(
() => () =>
(innerInputValue.value || '').length > props.maxLength || innerInputValue.value.length > props.maxLength ||
innerModelValue.value.some((item) => item.toString().length > props.maxLength) innerModelValue.value.some((item) => item.toString().length > props.maxLength)
); );
watch( watch(
@ -97,20 +99,6 @@
} }
); );
watch(
() => props.inputValue,
(val) => {
innerInputValue.value = val;
}
);
watch(
() => innerInputValue.value,
(val) => {
emit('update:inputValue', val);
}
);
function validateTagsCountEnter() { function validateTagsCountEnter() {
if (innerModelValue.value.length > 10) { if (innerModelValue.value.length > 10) {
innerModelValue.value.pop(); innerModelValue.value.pop();
@ -160,7 +148,8 @@
if ( if (
validateTagsCountEnter() && validateTagsCountEnter() &&
validateUniqueValue() && validateUniqueValue() &&
(innerInputValue.value || '').trim().length <= props.maxLength innerInputValue.value &&
innerInputValue.value.trim().length <= props.maxLength
) { ) {
innerInputValue.value = ''; innerInputValue.value = '';
tagsLength.value += 1; tagsLength.value += 1;

View File

@ -220,9 +220,9 @@
} }
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
function copyVersion() { async function copyVersion() {
if (isSupported) { if (isSupported) {
copy(appStore.version); await copy(appStore.version);
Message.success(t('common.copySuccess')); Message.success(t('common.copySuccess'));
} else { } else {
Message.warning(t('common.copyNotSupport')); Message.warning(t('common.copyNotSupport'));

View File

@ -56,6 +56,7 @@ export enum ResponseComposition {
} }
// 接口响应体格式 // 接口响应体格式
export enum ResponseBodyFormat { export enum ResponseBodyFormat {
NONE = 'NONE',
JSON = 'JSON', JSON = 'JSON',
XML = 'XML', XML = 'XML',
RAW = 'RAW', RAW = 'RAW',
@ -70,7 +71,17 @@ export enum RequestDefinitionStatus {
} }
// 接口导入支持格式 // 接口导入支持格式
export enum RequestImportFormat { export enum RequestImportFormat {
SWAGGER = 'SWAGGER', SWAGGER = 'Swagger3',
// MeterSphere = 'MeterSphere',
// Postman= 'Postman',
// Plugin = 'Plugin',
// Jmeter = 'Jmeter',
// Har = 'Har',
}
// 接口导入方式
export enum RequestImportType {
API = 'API',
SCHEDULE = 'Schedule',
} }
// 接口认证设置类型 // 接口认证设置类型
export enum RequestAuthType { export enum RequestAuthType {

View File

@ -254,14 +254,13 @@ export interface ExecuteConditionProcessorCommon {
export type ScriptProcessor = ScriptCommonConfig & ExecuteConditionProcessorCommon; export type ScriptProcessor = ScriptCommonConfig & ExecuteConditionProcessorCommon;
// 执行请求-前后置条件-SQL脚本处理器 // 执行请求-前后置条件-SQL脚本处理器
export interface SQLProcessor extends ExecuteConditionProcessorCommon { export interface SQLProcessor extends ExecuteConditionProcessorCommon {
description: string; // 描述 name: string; // 描述
dataSourceId: string; // 数据源ID dataSourceId: string; // 数据源ID
environmentId: string; // 环境ID dataSourceName: string; // 数据源名称
queryTimeout: number; // 超时时间 queryTimeout: number; // 超时时间
resultVariable: string; // 按结果存储时的结果变量 resultVariable: string; // 按结果存储时的结果变量
script: string; // 脚本内容 script: string; // 脚本内容
variableNames: string; // 按列存储时的变量名集合,多个列可以使用逗号分隔 variableNames: string; // 按列存储时的变量名集合,多个列可以使用逗号分隔
variables: EnableKeyValueParam[]; // 变量列表
extractParams: KeyValueParam[]; // 提取参数列表 extractParams: KeyValueParam[]; // 提取参数列表
} }
// 执行请求-前后置条件-等待时间处理器 // 执行请求-前后置条件-等待时间处理器

View File

@ -1,4 +1,6 @@
import { ModuleTreeNode, TableQueryParams } from '../common'; import { RequestDefinitionStatus, RequestImportFormat, RequestImportType } from '@/enums/apiEnum';
import { BatchApiParams, ModuleTreeNode, TableQueryParams } from '../common';
import { ExecuteRequestParams, ResponseDefinition } from './common'; import { ExecuteRequestParams, ResponseDefinition } from './common';
// 定义-自定义字段 // 定义-自定义字段
@ -13,7 +15,7 @@ export interface ApiDefinitionCreateParams extends ExecuteRequestParams {
tags: string[]; tags: string[];
response: ResponseDefinition; response: ResponseDefinition;
description: string; description: string;
status: string; status: RequestDefinitionStatus;
customFields: ApiDefinitionCustomField[]; customFields: ApiDefinitionCustomField[];
moduleId: string; moduleId: string;
versionId: string; versionId: string;
@ -22,10 +24,10 @@ export interface ApiDefinitionCreateParams extends ExecuteRequestParams {
} }
// 更新定义参数 // 更新定义参数
export interface ApiDefinitionUpdateParams extends ApiDefinitionCreateParams { export interface ApiDefinitionUpdateParams extends Partial<ApiDefinitionCreateParams> {
id: string; id: string;
deleteFileIds: string[]; deleteFileIds?: string[];
unLinkFileIds: string[]; unLinkFileIds?: string[];
} }
// 定义-自定义字段详情 // 定义-自定义字段详情
@ -164,3 +166,67 @@ export interface mockParams {
id: string; id: string;
projectId: string; projectId: string;
} }
// 批量操作参数
export interface ApiDefinitionBatchParams extends BatchApiParams {
protocol: string;
}
// 批量更新定义参数
export interface ApiDefinitionBatchUpdateParams extends ApiDefinitionBatchParams {
type?: string;
append?: boolean;
method?: string;
status?: RequestDefinitionStatus;
versionId?: string;
tags?: string[];
customField?: Record<string, any>;
}
// 批量移动定义参数
export interface ApiDefinitionBatchMoveParams extends ApiDefinitionBatchParams {
moduleId: string | number;
}
// 批量删除定义参数
export interface ApiDefinitionBatchDeleteParams extends ApiDefinitionBatchParams {
deleteAll: boolean;
}
// 定义-定时同步-更新参数
export interface UpdateScheduleParams {
id: string;
taskId: string;
}
// 定义-定时同步-检查 url 是否存在参数
export interface CheckScheduleParams {
projectId: string;
swaggerUrl: string;
}
// 导入定义-request参数
export interface ImportApiDefinitionRequest {
userId: string;
versionId?: string;
updateVersionId?: string;
defaultVersion?: boolean;
platform: RequestImportFormat;
type: RequestImportType;
coverModule: boolean; // 是否覆盖子目录
coverData: boolean; // 是否覆盖数据
syncCase: boolean; // 是否同步导入用例
protocol: string;
authSwitch?: boolean;
authUsername?: string;
authPassword?: string;
uniquelyIdentifies?: string;
resourceId?: string;
swaggerUrl?: string;
moduleId: string;
projectId: string;
name?: string;
}
// 导入定义参数
export interface ImportApiDefinitionParams {
file: File | null;
request: ImportApiDefinitionRequest;
}
// 导入定义-创建定时同步参数
export interface CreateImportApiDefinitionScheduleParams extends ImportApiDefinitionRequest {
value: string; // cron 表达式
config?: string;
}

View File

@ -135,7 +135,6 @@ export interface BatchReviewCaseParams extends BatchApiParams {
status: ReviewResult; // 评审结果 status: ReviewResult; // 评审结果
content: string; // 评论内容 content: string; // 评论内容
notifier: string; // 评论@的人的Id, 多个以';'隔开 notifier: string; // 评论@的人的Id, 多个以';'隔开
moduleIds: (string | number)[];
} }
// 评审详情-批量修改评审人 // 评审详情-批量修改评审人
export interface BatchChangeReviewerParams extends BatchApiParams { export interface BatchChangeReviewerParams extends BatchApiParams {
@ -143,13 +142,11 @@ export interface BatchChangeReviewerParams extends BatchApiParams {
userId: string; // 用户id, 用来判断是否只看我的 userId: string; // 用户id, 用来判断是否只看我的
reviewerId: string[]; // 评审人员id reviewerId: string[]; // 评审人员id
append: boolean; // 是否追加 append: boolean; // 是否追加
moduleIds: (string | number)[];
} }
// 评审详情-批量取消关联用例 // 评审详情-批量取消关联用例
export interface BatchCancelReviewCaseParams extends BatchApiParams { export interface BatchCancelReviewCaseParams extends BatchApiParams {
reviewId: string; // 评审id reviewId: string; // 评审id
userId: string; // 用户id, 用来判断是否只看我的 userId: string; // 用户id, 用来判断是否只看我的
moduleIds: (string | number)[];
} }
export interface ReviewDetailReviewersItem { export interface ReviewDetailReviewersItem {
avatar: string; avatar: string;

View File

@ -44,6 +44,8 @@ export interface BatchApiParams {
selectAll: boolean; // 是否跨页全选,即选择当前筛选条件下的全部表格数据 selectAll: boolean; // 是否跨页全选,即选择当前筛选条件下的全部表格数据
condition: Record<string, any>; // 当前表格查询的筛选条件 condition: Record<string, any>; // 当前表格查询的筛选条件
currentSelectCount?: number; // 当前已选择的数量 currentSelectCount?: number; // 当前已选择的数量
projectId?: string; // 项目 ID
moduleIds?: (string | number)[]; // 模块 ID 集合
} }
// 移动模块树 // 移动模块树

View File

@ -155,3 +155,16 @@ export interface HttpForm {
condition: ''; condition: '';
}; };
} }
// 环境列表项
export interface EnvironmentItem {
id: string;
name: string;
projectId: string;
createUser: string;
updateUser: string;
createTime: number;
updateTime: number;
mock: boolean;
description: string;
pos: number;
}

View File

@ -217,14 +217,15 @@
<div class="mb-[16px]"> <div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div> <div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
<a-input <a-input
v-model:model-value="condition.description" v-model:model-value="condition.name"
:placeholder="t('apiTestDebug.commonPlaceholder')" :placeholder="t('apiTestDebug.commonPlaceholder')"
:max-length="255" :max-length="255"
@input="() => emit('change')"
/> />
</div> </div>
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]"> <div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]"> <div class="text-[var(--color-text-2)]">
{{ condition.scriptName || '-' }} {{ condition.dataSourceName || '-' }}
</div> </div>
<a-divider margin="8px" direction="vertical" /> <a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="quoteSqlSourceDrawerVisible = true"> <MsButton type="text" class="font-medium" @click="quoteSqlSourceDrawerVisible = true">
@ -240,48 +241,52 @@
:language="LanguageEnum.SQL" :language="LanguageEnum.SQL"
:show-full-screen="false" :show-full-screen="false"
:show-theme-change="false" :show-theme-change="false"
read-only @change="() => emit('change')"
> >
</MsCodeEditor> </MsCodeEditor>
</div> </div>
<div class="mb-[16px]"> <div class="mb-[16px]">
<div class="mb-[8px] flex items-center text-[var(--color-text-1)]"> <div class="mb-[8px] flex items-center text-[var(--color-text-1)]">
{{ t('apiTestDebug.storageType') }} {{ t('apiTestDebug.storageByCol') }}
<a-tooltip position="right"> <a-tooltip position="right" :content="t('apiTestDebug.storageColTip')">
<icon-question-circle <icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]" class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16" size="16"
/> />
<template #content>
<div>{{ t('apiTestDebug.storageTypeTip1') }}</div>
<div>{{ t('apiTestDebug.storageTypeTip2') }}</div>
</template>
</a-tooltip> </a-tooltip>
</div> </div>
</div>
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByCol') }}</div>
<a-input <a-input
v-model:model-value="condition.variableNames" v-model:model-value="condition.variableNames"
:max-length="255" :max-length="255"
:placeholder="t('apiTestDebug.storageByColPlaceholder', { a: '{id_1}', b: '{username_1}' })" :placeholder="t('apiTestDebug.storageByColPlaceholder', { a: '{id_1}', b: '{username_1}' })"
@input="() => emit('change')"
/> />
</div> </div>
<div class="sql-table-container"> <div class="sql-table-container">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.extractParameter') }}</div> <div class="mb-[8px] flex items-center text-[var(--color-text-1)]">
{{ t('apiTestDebug.extractParameter') }}
<a-tooltip position="right" :content="t('apiTestDebug.storageResultTip')">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<paramTable <paramTable
v-model:params="condition.variables" v-model:params="condition.extractParams"
:columns="sqlSourceColumns" :columns="sqlSourceColumns"
:selectable="false" :selectable="false"
:default-param-item="defaultKeyValueParamItem"
@change="handleSqlSourceParamTableChange" @change="handleSqlSourceParamTableChange"
/> />
</div> </div>
<div class="mb-[16px]"> <div class="mt-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div> <div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div>
<a-input <a-input
v-model:model-value="condition.resultVariable" v-model:model-value="condition.resultVariable"
:max-length="255" :max-length="255"
:placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })" :placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })"
@input="() => emit('change')"
/> />
</div> </div>
</template> </template>
@ -431,6 +436,7 @@
import type { ProtocolItem } from '@/models/apiTest/common'; import type { ProtocolItem } from '@/models/apiTest/common';
import { ExecuteConditionProcessor, JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common'; import { ExecuteConditionProcessor, JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common';
import { ParamsRequestType } from '@/models/projectManagement/commonScript'; import { ParamsRequestType } from '@/models/projectManagement/commonScript';
import { DataSourceItem, EnvConfig } from '@/models/projectManagement/environmental';
import { import {
RequestConditionProcessor, RequestConditionProcessor,
RequestExtractEnvType, RequestExtractEnvType,
@ -441,6 +447,8 @@
ResponseBodyXPathAssertionFormat, ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
import { defaultKeyValueParamItem } from '@/views/api-test/components/config';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>; export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const appStore = useAppStore(); const appStore = useAppStore();
const props = withDefaults( const props = withDefaults(
@ -468,7 +476,31 @@
const { t } = useI18n(); const { t } = useI18n();
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const condition = useVModel(props, 'data', emit); const condition = useVModel(props, 'data', emit);
watchEffect(() => {
if (condition.value.processorType === RequestConditionProcessor.SQL && condition.value.dataSourceId) {
// SQL
const dataSourceItem = currentEnvConfig?.value.dataSources.find(
(item) => item.dataSource === condition.value.dataSourceName
);
if (dataSourceItem) {
//
condition.value.dataSourceName = dataSourceItem.dataSource;
condition.value.dataSourceId = dataSourceItem.id;
} else if (currentEnvConfig && currentEnvConfig.value.dataSources.length > 0) {
//
condition.value.dataSourceName = currentEnvConfig.value.dataSources[0].dataSource;
condition.value.dataSourceId = currentEnvConfig.value.dataSources[0].id;
} else {
//
condition.value.dataSourceName = '';
condition.value.dataSourceId = '';
}
}
});
// //
const isShowEditScriptNameInput = ref(false); const isShowEditScriptNameInput = ref(false);
const scriptNameInputRef = ref<InputInstance>(); const scriptNameInputRef = ref<InputInstance>();
@ -490,9 +522,9 @@ if (!result){
}`); }`);
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
function copyScriptEx() { async function copyScriptEx() {
if (isSupported) { if (isSupported) {
copy(scriptEx.value); await copy(scriptEx.value);
Message.success(t('apiTestDebug.scriptExCopySuccess')); Message.success(t('apiTestDebug.scriptExCopySuccess'));
} else { } else {
Message.warning(t('apiTestDebug.copyNotSupport')); Message.warning(t('apiTestDebug.copyNotSupport'));
@ -598,14 +630,14 @@ if (!result){
}, },
]; ];
const quoteSqlSourceDrawerVisible = ref(false); const quoteSqlSourceDrawerVisible = ref(false);
function handleQuoteSqlSourceApply(sqlSource: Record<string, any>) { function handleQuoteSqlSourceApply(sqlSource: DataSourceItem) {
condition.value.script = sqlSource.script; condition.value.dataSourceName = sqlSource.dataSource;
condition.value.dataSourceId = sqlSource.id; condition.value.dataSourceId = sqlSource.id;
emit('change'); emit('change');
} }
function handleSqlSourceParamTableChange(resultArr: any[], isInit?: boolean) { function handleSqlSourceParamTableChange(resultArr: any[], isInit?: boolean) {
condition.value.variables = [...resultArr]; condition.value.extractParams = [...resultArr];
if (!isInit) { if (!isInit) {
emit('change'); emit('change');
} }

View File

@ -169,15 +169,13 @@
associateScenarioResult: false, associateScenarioResult: false,
ignoreProtocols: [], ignoreProtocols: [],
beforeStepScript: true, beforeStepScript: true,
description: '', name: '',
enable: true, enable: true,
dataSourceId: '', dataSourceId: '',
environmentId: '',
queryTimeout: 0, queryTimeout: 0,
resultVariable: '', resultVariable: '',
script: '', script: '',
variableNames: '', variableNames: '',
variables: [],
extractParams: [], extractParams: [],
}); });

View File

@ -2,6 +2,7 @@ import {
EnableKeyValueParam, EnableKeyValueParam,
ExecuteRequestCommonParam, ExecuteRequestCommonParam,
ExecuteRequestFormBodyFormValue, ExecuteRequestFormBodyFormValue,
KeyValueParam,
ResponseDefinition, ResponseDefinition,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { RequestContentTypeEnum, RequestParamsType, ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum'; import { RequestContentTypeEnum, RequestParamsType, ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
@ -74,5 +75,11 @@ export const defaultResponseItem: ResponseDefinition = {
}, },
}; };
// 默认提取参数的 key-value 表格行的值
export const defaultKeyValueParamItem: KeyValueParam = {
key: '',
value: '',
};
// 请求的响应 response 的响应状态码集合 // 请求的响应 response 的响应状态码集合
export const statusCodes = [200, 201, 202, 203, 204, 205, 400, 401, 402, 403, 404, 405, 500, 501, 502, 503, 504, 505]; export const statusCodes = [200, 201, 202, 203, 204, 205, 400, 401, 402, 403, 404, 405, 500, 501, 502, 503, 504, 505];

View File

@ -1,12 +1,5 @@
<template> <template>
<MsBaseTable <MsFormTable v-bind="props" :data="paramsData">
v-bind="propsRes"
:hoverable="false"
no-disable
is-simple-setting
:span-method="props.spanMethod"
v-on="propsEvent"
>
<!-- 展开行--> <!-- 展开行-->
<template #expand-icon="{ record }"> <template #expand-icon="{ record }">
<div class="flex flex-row items-center gap-[2px] text-[var(--color-text-4)]"> <div class="flex flex-row items-center gap-[2px] text-[var(--color-text-4)]">
@ -404,7 +397,7 @@
/> />
</div> </div>
</template> </template>
</MsBaseTable> </MsFormTable>
<a-modal <a-modal
v-model:visible="showQuickInputParam" v-model:visible="showQuickInputParam"
:title="t('ms.paramsInput.value')" :title="t('ms.paramsInput.value')"
@ -463,9 +456,7 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import type { MsTableColumnData } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue'; import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
@ -476,13 +467,12 @@
import { groupProjectEnv, listEnv } from '@/api/modules/project-management/envManagement'; import { groupProjectEnv, listEnv } from '@/api/modules/project-management/envManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ModuleTreeNode, TransferFileParams } from '@/models/common'; import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { ProjectOptionItem } from '@/models/projectManagement/environmental'; import { ProjectOptionItem } from '@/models/projectManagement/environmental';
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
import { filterKeyValParams } from './utils'; import { filterKeyValParams } from './utils';
import { TableOperationColumn } from '@arco-design/web-vue/es/table/interface'; import { TableOperationColumn } from '@arco-design/web-vue/es/table/interface';
@ -490,7 +480,7 @@
const MsAddAttachment = defineAsyncComponent(() => import('@/components/business/ms-add-attachment/index.vue')); const MsAddAttachment = defineAsyncComponent(() => import('@/components/business/ms-add-attachment/index.vue'));
const MsParamsInput = defineAsyncComponent(() => import('@/components/business/ms-params-input/index.vue')); const MsParamsInput = defineAsyncComponent(() => import('@/components/business/ms-params-input/index.vue'));
export type ParamTableColumn = MsTableColumnData & { export interface ParamTableColumn extends FormTableColumn {
isNormal?: boolean; // value MsParamsInput isNormal?: boolean; // value MsParamsInput
hasRequired?: boolean; // type required hasRequired?: boolean; // type required
typeOptions?: { label: string; value: string }[]; // type typeOptions?: { label: string; value: string }[]; // type
@ -499,7 +489,7 @@
moreAction?: ActionsItem[]; // operation moreAction?: ActionsItem[]; // operation
format?: RequestBodyFormat; // operation format?: RequestBodyFormat; // operation
addLineDisabled?: boolean; // addLineDisabled?: boolean; //
}; }
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -565,75 +555,22 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const tableStore = useTableStore(); const paramsData = ref<any[]>(props.params);
async function initColumns() {
if (props.showSetting && props.tableKey) {
await tableStore.initColumn(props.tableKey, props.columns);
}
}
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
firstColumnWidth: 32,
tableKey: props.showSetting ? props.tableKey : undefined,
scroll: props.scroll,
heightUsed: props.heightUsed,
columns: props.columns,
selectable: props.selectable,
draggable: props.draggable ? { type: 'handle', width: 24 } : undefined,
showSetting: props.showSetting,
disabled: props.disabled,
showSelectorAll: props.showSelectorAll,
isSimpleSetting: props.isSimpleSetting,
showPagination: false,
});
function emitChange(from: string, isInit?: boolean) { function emitChange(from: string, isInit?: boolean) {
if (!isInit) { if (!isInit) {
emit('change', propsRes.value.data); emit('change', paramsData.value);
} }
} }
const selectedKeys = computed(() => propsRes.value.data.filter((e) => e.enable).map((e) => e.id)); const paramsLength = computed(() => paramsData.value.length);
propsEvent.value.rowSelectChange = (key: string) => {
propsRes.value.data = propsRes.value.data.map((e) => {
if (e.id === key) {
e.enable = !e.enable;
}
return e;
});
emitChange('rowSelectChange');
};
propsEvent.value.selectAllChange = (v: SelectAllEnum) => {
propsRes.value.data = propsRes.value.data.map((e) => {
e.enable = v !== SelectAllEnum.NONE;
return e;
});
emitChange('selectAllChange');
};
watch(
() => selectedKeys.value,
(arr) => {
propsRes.value.selectedKeys = new Set(arr);
}
);
watch(
() => props.heightUsed,
(val) => {
propsRes.value.heightUsed = val;
}
);
const paramsLength = computed(() => propsRes.value.data.length);
function deleteParam(record: Record<string, any>, rowIndex: number) { function deleteParam(record: Record<string, any>, rowIndex: number) {
if (props.isTreeTable) { if (props.isTreeTable) {
emit('treeDelete', record); emit('treeDelete', record);
return; return;
} }
propsRes.value.data.splice(rowIndex, 1); paramsData.value.splice(rowIndex, 1);
emitChange('deleteParam'); emitChange('deleteParam');
} }
@ -644,17 +581,14 @@
const handleMustIncludeChange = (val: boolean) => { const handleMustIncludeChange = (val: boolean) => {
mustIncludeAllChecked.value = val; mustIncludeAllChecked.value = val;
mustIncludeIndeterminate.value = false; mustIncludeIndeterminate.value = false;
const { data } = propsRes.value; paramsData.value.forEach((e: any) => {
data.forEach((e: any) => {
e.mustInclude = val; e.mustInclude = val;
}); });
propsRes.value.data = data;
emitChange('handleMustIncludeChange'); emitChange('handleMustIncludeChange');
}; };
const handleMustContainColChange = (notEmit?: boolean) => { const handleMustContainColChange = (notEmit?: boolean) => {
const { data } = propsRes.value; const checkedList = paramsData.value.filter((e: any) => e.mustInclude).map((e: any) => e.id);
const checkedList = data.filter((e: any) => e.mustInclude).map((e: any) => e.id); if (checkedList.length === paramsData.value.length) {
if (checkedList.length === data.length) {
mustIncludeAllChecked.value = true; mustIncludeAllChecked.value = true;
mustIncludeIndeterminate.value = false; mustIncludeIndeterminate.value = false;
} else if (checkedList.length === 0) { } else if (checkedList.length === 0) {
@ -674,17 +608,14 @@
const handleTypeCheckingChange = (val: boolean) => { const handleTypeCheckingChange = (val: boolean) => {
typeCheckingAllChecked.value = val; typeCheckingAllChecked.value = val;
typeCheckingIndeterminate.value = false; typeCheckingIndeterminate.value = false;
const { data } = propsRes.value; paramsData.value.forEach((e: any) => {
data.forEach((e: any) => {
e.typeChecking = val; e.typeChecking = val;
}); });
propsRes.value.data = data;
emitChange('handleTypeCheckingChange'); emitChange('handleTypeCheckingChange');
}; };
const handleTypeCheckingColChange = (notEmit?: boolean) => { const handleTypeCheckingColChange = (notEmit?: boolean) => {
const { data } = propsRes.value; const checkedList = paramsData.value.filter((e: any) => e.typeChecking).map((e: any) => e.id);
const checkedList = data.filter((e: any) => e.typeChecking).map((e: any) => e.id); if (checkedList.length === paramsData.value.length) {
if (checkedList.length === data.length) {
typeCheckingAllChecked.value = true; typeCheckingAllChecked.value = true;
typeCheckingIndeterminate.value = false; typeCheckingIndeterminate.value = false;
} else if (checkedList.length === 0) { } else if (checkedList.length === 0) {
@ -766,10 +697,10 @@
if (addLineDisabled) { if (addLineDisabled) {
return; return;
} }
if (rowIndex === propsRes.value.data.length - 1) { if (rowIndex === paramsData.value.length - 1) {
// //
const id = new Date().getTime().toString(); const id = new Date().getTime().toString();
propsRes.value.data.push({ paramsData.value.push({
id, id,
...cloneDeep(props.defaultParamItem), // ...cloneDeep(props.defaultParamItem), //
enable: true, // enable: true, //
@ -785,7 +716,14 @@
(arr) => { (arr) => {
if (arr.length > 0) { if (arr.length > 0) {
let hasNoIdItem = false; // id let hasNoIdItem = false; // id
propsRes.value.data = arr.map((item, i) => { paramsData.value = arr.map((item, i) => {
if (!item) {
// undefined
return {
...props.defaultParamItem,
id: new Date().getTime() + i,
};
}
if (!item.id) { if (!item.id) {
// id // id
hasNoIdItem = true; hasNoIdItem = true;
@ -801,7 +739,7 @@
} }
} else { } else {
const id = new Date().getTime().toString(); const id = new Date().getTime().toString();
propsRes.value.data = [ paramsData.value = [
{ {
id, // id props.defaultParamItem id id, // id props.defaultParamItem id
...props.defaultParamItem, ...props.defaultParamItem,
@ -880,7 +818,7 @@
function applyQuickInputParam() { function applyQuickInputParam() {
activeQuickInputRecord.value.value = quickInputParamValue.value; activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false; showQuickInputParam.value = false;
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id)); addTableLine(paramsData.value.findIndex((e) => e.id === activeQuickInputRecord.value.id));
clearQuickInputParam(); clearQuickInputParam();
emitChange('applyQuickInputParam'); emitChange('applyQuickInputParam');
} }
@ -902,7 +840,7 @@
function applyQuickInputDesc() { function applyQuickInputDesc() {
activeQuickInputRecord.value.description = quickInputDescValue.value; activeQuickInputRecord.value.description = quickInputDescValue.value;
showQuickInputDesc.value = false; showQuickInputDesc.value = false;
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id)); addTableLine(paramsData.value.findIndex((e) => e.id === activeQuickInputRecord.value.id));
clearQuickInputDesc(); clearQuickInputDesc();
emitChange('applyQuickInputDesc'); emitChange('applyQuickInputDesc');
} }
@ -959,61 +897,9 @@
defineExpose({ defineExpose({
addTableLine, addTableLine,
}); });
await initColumns();
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
line-height: normal;
}
:deep(.arco-table .arco-table-cell) {
padding: 8px 2px;
}
:deep(.arco-table-cell-align-left) {
padding: 8px;
}
:deep(.arco-table-col-fixed-right) {
.arco-table-cell-align-left {
padding: 8px;
}
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
border-color: transparent !important;
.arco-input::placeholder {
@apply invisible;
}
.arco-select-view-icon {
@apply invisible;
}
.arco-select-view-value {
color: var(--color-text-1);
}
.arco-select {
border-color: transparent !important;
}
}
}
:deep(.param-input-number) {
@apply pr-0;
.arco-input {
@apply text-right;
}
.arco-input-suffix {
@apply hidden;
}
&:hover,
&.arco-input-focus {
.arco-input {
@apply text-left;
}
.arco-input-suffix {
@apply inline-flex;
}
}
}
.content-type-trigger-content { .content-type-trigger-content {
@apply bg-white; @apply bg-white;
@ -1043,7 +929,4 @@
line-height: 16px; line-height: 16px;
color: var(--color-text-1); color: var(--color-text-1);
} }
:deep(.arco-table-expand-btn) {
background: transparent;
}
</style> </style>

View File

@ -13,8 +13,9 @@
:placeholder="t('project.projectVersion.searchPlaceholder')" :placeholder="t('project.projectVersion.searchPlaceholder')"
class="w-[230px]" class="w-[230px]"
allow-clear allow-clear
@search="searchSource" @search="searchDataSource"
@press-enter="searchSource" @press-enter="searchDataSource"
@clear="searchDataSource"
/> />
</div> </div>
<MsBaseTable v-bind="propsRes" v-model:selected-key="selectedKey" v-on="propsEvent"> <MsBaseTable v-bind="propsRes" v-model:selected-key="selectedKey" v-on="propsEvent">
@ -29,6 +30,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { cloneDeep } from 'lodash-es';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
@ -37,6 +39,8 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { EnvConfig } from '@/models/projectManagement/environmental';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
selectedKey?: string; selectedKey?: string;
@ -48,6 +52,8 @@
const { t } = useI18n(); const { t } = useI18n();
/** 接收祖先组件提供的属性 */
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const innerVisible = useVModel(props, 'visible', emit); const innerVisible = useVModel(props, 'visible', emit);
const keyword = ref(''); const keyword = ref('');
const selectedKey = ref(props.selectedKey || ''); const selectedKey = ref(props.selectedKey || '');
@ -55,7 +61,7 @@
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'apiTestDebug.sqlSourceName', title: 'apiTestDebug.sqlSourceName',
dataIndex: 'name', dataIndex: 'dataSource',
showTooltip: true, showTooltip: true,
}, },
{ {
@ -71,7 +77,7 @@
}, },
{ {
title: 'apiTestDebug.maxConnection', title: 'apiTestDebug.maxConnection',
dataIndex: 'maxConnection', dataIndex: 'poolMax',
width: 140, width: 140,
}, },
{ {
@ -82,47 +88,8 @@
width: 120, width: 120,
}, },
]; ];
async function loadSource() {
return Promise.resolve({ const { propsRes, propsEvent } = useTable(undefined, {
list: [
{
id: '1',
name: 'test',
driver: 'com.mysql.cj.jdbc.Driver',
username: 'root',
maxConnection: 10,
timeout: 1000,
storageType: 'column',
params: [],
script: 'select * from test1',
},
{
id: '2',
name: 'test2',
driver: 'com.mysql.cj.jdbc.Driver',
username: 'root',
maxConnection: 10,
timeout: 1000,
storageType: 'column',
params: [],
script: 'select * from test2',
},
{
id: '3',
name: 'test3',
driver: 'com.mysql.cj.jdbc.Driver',
username: 'root',
maxConnection: 10,
timeout: 10000000000,
storageType: 'result',
params: [],
script: 'select * from test3',
},
],
total: 99,
});
}
const { propsRes, propsEvent, setLoadListParams, loadList } = useTable(loadSource, {
columns, columns,
scroll: { x: '100%' }, scroll: { x: '100%' },
heightUsed: 300, heightUsed: 300,
@ -130,14 +97,28 @@
showSelectorAll: false, showSelectorAll: false,
selectorType: 'radio', selectorType: 'radio',
firstColumnWidth: 44, firstColumnWidth: 44,
showPagination: false,
}); });
function searchSource() {
setLoadListParams({ watch(
keyword: keyword.value, () => currentEnvConfig?.value,
}); (config) => {
loadList(); if (config) {
propsRes.value.data = cloneDeep(config.dataSources) as any[];
}
},
{
immediate: true,
}
);
function searchDataSource() {
if (keyword.value.trim() !== '') {
propsRes.value.data = propsRes.value.data.filter((e) => e.dataSource.includes(keyword.value));
} else {
propsRes.value.data = cloneDeep(currentEnvConfig?.value.dataSources) as any[];
}
} }
searchSource();
function handleConfirm() { function handleConfirm() {
innerVisible.value = false; innerVisible.value = false;

View File

@ -339,6 +339,7 @@
import { import {
ExecuteApiRequestFullParams, ExecuteApiRequestFullParams,
ExecuteConditionConfig,
ExecuteRequestParams, ExecuteRequestParams,
PluginConfig, PluginConfig,
RequestTaskResult, RequestTaskResult,
@ -348,6 +349,7 @@
RequestAuthType, RequestAuthType,
RequestBodyFormat, RequestBodyFormat,
RequestComposition, RequestComposition,
RequestConditionProcessor,
RequestMethods, RequestMethods,
RequestParamsType, RequestParamsType,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
@ -356,6 +358,7 @@
import { import {
defaultBodyParamsItem, defaultBodyParamsItem,
defaultHeaderParamsItem, defaultHeaderParamsItem,
defaultKeyValueParamItem,
defaultRequestParamsItem, defaultRequestParamsItem,
} from '@/views/api-test/components/config'; } from '@/views/api-test/components/config';
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils'; import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
@ -849,6 +852,20 @@
} }
); );
function filterConditionsSqlValidParams(condition: ExecuteConditionConfig) {
const conditionCopy = cloneDeep(condition);
conditionCopy.processors = conditionCopy.processors.map((processor) => {
if (processor.processorType === RequestConditionProcessor.SQL) {
processor.extractParams = filterKeyValParams(
processor.extractParams || [],
defaultKeyValueParamItem
).validParams;
}
return processor;
});
return conditionCopy;
}
/** /**
* 生成请求参数 * 生成请求参数
* @param executeType 执行类型执行时传入 * @param executeType 执行类型执行时传入
@ -930,12 +947,11 @@
{ {
polymorphicName: 'MsCommonElement', // MsCommonElement polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: { assertionConfig: {
// TODO:
enableGlobal: false, enableGlobal: false,
assertions: [], assertions: [],
}, },
postProcessorConfig: requestVModel.value.children[0].postProcessorConfig, postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
preProcessorConfig: requestVModel.value.children[0].preProcessorConfig, preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
}, },
], ],
}, },

View File

@ -193,6 +193,15 @@
}); });
const activeResponse = ref<ResponseItem>(responseTabs.value[0]); const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
watch(
() => responseTabs.value,
(arr) => {
if (arr[0]) {
[activeResponse.value] = arr;
}
}
);
function addResponseTab(defaultProps?: Partial<ResponseItem>) { function addResponseTab(defaultProps?: Partial<ResponseItem>) {
responseTabs.value.push({ responseTabs.value.push({
...cloneDeep(defaultResponseItem), ...cloneDeep(defaultResponseItem),

View File

@ -117,8 +117,8 @@
</div> </div>
<a-spin :loading="props.loading" class="h-[calc(100%-35px)] w-full px-[18px] pb-[18px]"> <a-spin :loading="props.loading" class="h-[calc(100%-35px)] w-full px-[18px] pb-[18px]">
<edit <edit
v-if="props.isEdit && activeResponseType === 'content' && props.responseDefinition" v-if="props.isEdit && activeResponseType === 'content' && validResponseDefinition"
:response-definition="props.responseDefinition" :response-definition="validResponseDefinition"
:upload-temp-file-api="props.uploadTempFileApi" :upload-temp-file-api="props.uploadTempFileApi"
@change="handleResponseChange" @change="handleResponseChange"
/> />
@ -144,7 +144,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { RequestTaskResult } from '@/models/apiTest/common'; import { RequestTaskResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum'; import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -215,6 +215,45 @@
} }
return ''; return '';
}); });
//
const validResponseDefinition = computed(() => {
return props.responseDefinition?.map((item, i) => {
// null
if (!item.headers) {
item.headers = [];
}
if (!item.id) {
item.id = new Date().getTime() + i;
}
if (item.body.bodyType === ResponseBodyFormat.NONE) {
item.body.bodyType = ResponseBodyFormat.RAW;
}
if (!item.body.binaryBody) {
item.body.binaryBody = {
description: '',
file: undefined,
};
}
if (!item.body.jsonBody) {
item.body.jsonBody = {
jsonValue: '',
enableJsonSchema: false,
enableTransition: false,
};
if (!item.body.xmlBody) {
item.body.xmlBody = {
value: '',
};
}
if (!item.body.rawBody) {
item.body.rawBody = {
value: '',
};
}
}
return item;
});
});
function handleResponseChange() { function handleResponseChange() {
emit('change'); emit('change');
@ -229,7 +268,7 @@
watch( watch(
() => props.requestTaskResult, () => props.requestTaskResult,
(task) => { (task) => {
if (task) { if (task?.requestResults[0]?.responseResult?.responseCode) {
setActiveResponse('result'); setActiveResponse('result');
} }
} }

View File

@ -131,9 +131,9 @@
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
function copyScript() { async function copyScript() {
if (isSupported) { if (isSupported) {
copy(props.requestResult?.responseResult.body || ''); await copy(props.requestResult?.responseResult.body || '');
Message.success(t('common.copySuccess')); Message.success(t('common.copySuccess'));
} else { } else {
Message.warning(t('apiTestDebug.copyNotSupport')); Message.warning(t('apiTestDebug.copyNotSupport'));

View File

@ -3,8 +3,6 @@ import { cloneDeep, isEqual } from 'lodash-es';
import { ExecuteBody } from '@/models/apiTest/common'; import { ExecuteBody } from '@/models/apiTest/common';
import { RequestParamsType } from '@/enums/apiEnum'; import { RequestParamsType } from '@/enums/apiEnum';
export default {};
export interface ParseResult { export interface ParseResult {
uploadFileIds: string[]; uploadFileIds: string[];
linkFileIds: string[]; linkFileIds: string[];
@ -108,7 +106,7 @@ export function parseRequestBodyFiles(
* @param params * @param params
* @param defaultParamItem * @param defaultParamItem
*/ */
export function filterKeyValParams(params: Record<string, any>[], defaultParamItem: Record<string, any>) { export function filterKeyValParams<T>(params: (T & Record<string, any>)[], defaultParamItem: Record<string, any>) {
const lastData = cloneDeep(params[params.length - 1]); const lastData = cloneDeep(params[params.length - 1]);
const defaultParam = cloneDeep(defaultParamItem); const defaultParam = cloneDeep(defaultParamItem);
if (!lastData || !defaultParam) { if (!lastData || !defaultParam) {
@ -123,7 +121,7 @@ export function filterKeyValParams(params: Record<string, any>[], defaultParamIt
delete defaultParam.id; delete defaultParam.id;
delete defaultParam.enable; delete defaultParam.enable;
const lastDataIsDefault = isEqual(lastData, defaultParam); const lastDataIsDefault = isEqual(lastData, defaultParam);
let validParams: Record<string, any>[] = []; let validParams: (T & Record<string, any>)[];
if (lastDataIsDefault) { if (lastDataIsDefault) {
// 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个 // 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个
validParams = params.slice(0, params.length - 1); validParams = params.slice(0, params.length - 1);

View File

@ -249,7 +249,6 @@
return { return {
...e, ...e,
hideMoreAction: e.id === 'root', hideMoreAction: e.id === 'root',
draggable: e.id !== 'root',
}; };
}); });
rootModulesName.value = folderTree.value.map((e) => e.name || ''); rootModulesName.value = folderTree.value.map((e) => e.name || '');
@ -381,6 +380,7 @@
} }
if (dropNode.type === 'MODULE' && dragNode?.type === 'API' && dropPosition !== 0) { if (dropNode.type === 'MODULE' && dragNode?.type === 'API' && dropPosition !== 0) {
// API // API
document.querySelector('.arco-tree-node-title-draggable::before')?.setAttribute('style', 'display: none');
return false; return false;
} }
return true; return true;
@ -400,6 +400,10 @@
dropPosition: number dropPosition: number
) { ) {
try { try {
if (dragNode.id === 'root' || (dragNode.type === 'MODULE' && dropNode.id === 'root')) {
//
return;
}
loading.value = true; loading.value = true;
if (dragNode.type === 'MODULE') { if (dragNode.type === 'MODULE') {
await moveDebugModule({ await moveDebugModule({
@ -411,8 +415,8 @@
await dragDebug({ await dragDebug({
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moveMode: dropPositionMap[dropPosition], moveMode: dropPositionMap[dropPosition],
moveId: dropNode.id, moveId: dragNode.id,
targetId: dragNode.id, targetId: dropNode.id,
moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id, // APIidid moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id, // APIidid
}); });
} }

View File

@ -103,12 +103,12 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import debug, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import debug, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { import {
addDebug, addDebug,
executeDebug, executeDebug,
getDebugDetail, getDebugDetail,
getTransferOptions, getTransferOptions,
localExecuteApiDebug,
transferFile, transferFile,
updateDebug, updateDebug,
uploadTempFile, uploadTempFile,

View File

@ -86,11 +86,10 @@ export default {
'apiTestDebug.quoteSource': 'Reference data source', 'apiTestDebug.quoteSource': 'Reference data source',
'apiTestDebug.sourceList': 'Data source list', 'apiTestDebug.sourceList': 'Data source list',
'apiTestDebug.quoteSourcePlaceholder': 'Please select a data source', 'apiTestDebug.quoteSourcePlaceholder': 'Please select a data source',
'apiTestDebug.storageType': 'Storage method', 'apiTestDebug.storageColTip':
'apiTestDebug.storageTypeTip1': 'Specify the names of columns extracted from the database result set; multiple columns can be separated by ","',
'Store by column: Specify the names of columns extracted from the database result set; multiple columns can be separated by ","', 'apiTestDebug.storageResultTip':
'apiTestDebug.storageTypeTip2': 'Save the entire result set as a variable instead of saving each column value as a separate variable',
'Store by result: Save the entire result set as a variable instead of saving each column value as a separate variable',
'apiTestDebug.storageByCol': 'Store by columns', 'apiTestDebug.storageByCol': 'Store by columns',
'apiTestDebug.storageByColPlaceholder': 'For example, {a} is changed to {b}', 'apiTestDebug.storageByColPlaceholder': 'For example, {a} is changed to {b}',
'apiTestDebug.storageByResult': 'Store by result', 'apiTestDebug.storageByResult': 'Store by result',

View File

@ -82,9 +82,8 @@ export default {
'apiTestDebug.quoteSource': '引用数据源', 'apiTestDebug.quoteSource': '引用数据源',
'apiTestDebug.sourceList': '数据源列表', 'apiTestDebug.sourceList': '数据源列表',
'apiTestDebug.quoteSourcePlaceholder': '请选择数据源', 'apiTestDebug.quoteSourcePlaceholder': '请选择数据源',
'apiTestDebug.storageType': '存储方式', 'apiTestDebug.storageColTip': '指定从数据库结果集中提取的列的名称;多个列可以使用“,”分隔',
'apiTestDebug.storageTypeTip1': '按列存储:指定从数据库结果集中提取的列的名称;多个列可以使用“,”分隔', 'apiTestDebug.storageResultTip': '把整个结果集保存为一个变量,而不是将每个列的值保存为单独的变量',
'apiTestDebug.storageTypeTip2': '按结果存储:把整个结果集保存为一个变量,而不是将每个列的值保存为单独的变量',
'apiTestDebug.storageByCol': '按列存储', 'apiTestDebug.storageByCol': '按列存储',
'apiTestDebug.storageByColPlaceholder': '如 {a} 改成 {b}', 'apiTestDebug.storageByColPlaceholder': '如 {a} 改成 {b}',
'apiTestDebug.storageByResult': '按结果存储', 'apiTestDebug.storageByResult': '按结果存储',

View File

@ -1,21 +1,24 @@
<template> <template>
<div>
<MsDrawer <MsDrawer
v-model:visible="visible" v-model:visible="visible"
width="100%" width="100%"
:popup-container="props.popupContainer" :popup-container="props.popupContainer"
:closable="false" :closable="false"
:ok-disabled="disabledConfirm" :ok-disabled="disabledConfirm"
:ok-text="t('common.import')"
:ok-loading="importLoading"
disabled-width-drag disabled-width-drag
no-title desc
@confirm="confirmImport" @confirm="confirmImport"
@cancel="cancelImport" @cancel="cancelImport"
> >
<template #title> </template> <template #title> </template>
<div class="flex items-center justify-between p-[12px_8px]"> <div class="flex items-center justify-between p-[12px_8px]">
<div class="font-medium text-[var(--color-text-1)]">{{ t('apiTestManagement.importApi') }}</div> <div class="font-medium text-[var(--color-text-1)]">{{ t('apiTestManagement.importApi') }}</div>
<a-radio-group v-model:model-value="importType" type="button"> <a-radio-group v-model:model-value="importForm.type" type="button">
<a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio> <a-radio :value="RequestImportType.API">{{ t('apiTestManagement.fileImport') }}</a-radio>
<a-radio value="time">{{ t('apiTestManagement.timeImport') }}</a-radio> <a-radio :value="RequestImportType.SCHEDULE">{{ t('apiTestManagement.timeImport') }}</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div <div
@ -23,9 +26,9 @@
class="my-[16px] flex items-center gap-[16px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]" class="my-[16px] flex items-center gap-[16px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]"
> >
<div <div
v-for="item of importFormatList" v-for="item of platformList"
:key="item.value" :key="item.value"
:class="`import-item ${importFormat === item.value ? 'import-item--active' : ''}`" :class="`import-item ${importForm.platform === item.value ? 'import-item--active' : ''}`"
@click="() => setActiveImportFormat(item.value)" @click="() => setActiveImportFormat(item.value)"
> >
<div class="flex h-[24px] w-[24px] items-center justify-center rounded-[var(--border-radius-small)] bg-white"> <div class="flex h-[24px] w-[24px] items-center justify-center rounded-[var(--border-radius-small)] bg-white">
@ -35,10 +38,10 @@
</div> </div>
</div> </div>
<a-form ref="importFormRef" :model="importForm" layout="vertical"> <a-form ref="importFormRef" :model="importForm" layout="vertical">
<template v-if="importType === 'file'"> <template v-if="importForm.type === RequestImportType.API">
<a-form-item :label="t('apiTestManagement.belongModule')"> <a-form-item :label="t('apiTestManagement.belongModule')">
<a-tree-select <a-tree-select
v-model:modelValue="importForm.module" v-model:modelValue="importForm.moduleId"
:data="moduleTree" :data="moduleTree"
class="w-[436px]" class="w-[436px]"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :field-names="{ title: 'name', key: 'id', children: 'children' }"
@ -67,9 +70,9 @@
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-select v-model:model-value="importForm.mode" class="w-[240px]"> <a-select v-model:model-value="importForm.coverData" class="w-[240px]">
<a-option value="cover">{{ t('apiTestManagement.cover') }}</a-option> <a-option :value="true">{{ t('apiTestManagement.cover') }}</a-option>
<a-option value="uncover">{{ t('apiTestManagement.uncover') }}</a-option> <a-option :value="false">{{ t('apiTestManagement.uncover') }}</a-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false"> <a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false">
@ -85,24 +88,24 @@
</MsButton> </MsButton>
</template> </template>
<div class="mt-[16px]"> <div class="mt-[16px]">
<a-checkbox v-model:model-value="importForm.syncImportCase" class="mr-[24px]"> <a-checkbox v-model:model-value="importForm.syncCase" class="mr-[24px]">
{{ t('apiTestManagement.syncImportCase') }} {{ t('apiTestManagement.syncImportCase') }}
</a-checkbox> </a-checkbox>
<a-checkbox v-model:model-value="importForm.syncUpdateDirectory"> <a-checkbox v-model:model-value="importForm.coverModule">
{{ t('apiTestManagement.syncUpdateDirectory') }} {{ t('apiTestManagement.syncUpdateDirectory') }}
</a-checkbox> </a-checkbox>
</div> </div>
</a-collapse-item> </a-collapse-item>
</a-collapse> </a-collapse>
<a-form-item :label="t('apiTestManagement.importType')" class="mt-[8px]"> <a-form-item :label="t('apiTestManagement.importType')" class="mt-[8px]">
<a-radio-group v-model:model-value="importForm.importType" type="button"> <a-radio-group v-model:model-value="importType" type="button">
<a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio> <a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio>
<a-radio value="url">{{ t('apiTestManagement.urlImport') }}</a-radio> <a-radio value="swaggerUrl">{{ t('apiTestManagement.urlImport') }}</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<MsUpload <MsUpload
v-if="importForm.importType === 'file'" v-if="importType === 'file'"
v-model:file-list="importForm.file" v-model:file-list="fileList"
accept="json" accept="json"
:auto-upload="false" :auto-upload="false"
draggable draggable
@ -119,45 +122,45 @@
</MsUpload> </MsUpload>
<template v-else> <template v-else>
<a-form-item <a-form-item
field="url" field="swaggerUrl"
label="SwaggerURL" label="SwaggerURL"
asterisk-position="end" asterisk-position="end"
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
> >
<a-input <a-input
v-model:model-value="importForm.url" v-model:model-value="importForm.swaggerUrl"
:placeholder="t('apiTestManagement.urlImportPlaceholder')" :placeholder="t('apiTestManagement.urlImportPlaceholder')"
class="w-[700px]" class="w-[700px]"
allow-clear allow-clear
></a-input> ></a-input>
</a-form-item> </a-form-item>
<div class="mb-[16px] flex items-center gap-[8px]"> <div class="mb-[16px] flex items-center gap-[8px]">
<a-switch v-model:model-value="importForm.basicAuth" type="line" size="small"></a-switch> <a-switch v-model:model-value="importForm.authSwitch" type="line" size="small"></a-switch>
{{ t('apiTestManagement.basicAuth') }} {{ t('apiTestManagement.basicAuth') }}
</div> </div>
<template v-if="importForm.basicAuth"> <template v-if="importForm.authSwitch">
<a-form-item <a-form-item
field="account" field="authUsername"
:label="t('apiTestManagement.account')" :label="t('apiTestManagement.account')"
asterisk-position="end" asterisk-position="end"
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
> >
<a-input <a-input
v-model:model-value="importForm.account" v-model:model-value="importForm.authUsername"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
class="w-[500px]" class="w-[500px]"
allow-clear allow-clear
></a-input> ></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="password" field="authPassword"
:label="t('apiTestManagement.password')" :label="t('apiTestManagement.password')"
asterisk-position="end" asterisk-position="end"
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
autocomplete="new-password" autocomplete="new-password"
> >
<a-input-password <a-input-password
v-model:model-value="importForm.password" v-model:model-value="importForm.authPassword"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
class="w-[500px]" class="w-[500px]"
autocomplete="new-password" autocomplete="new-password"
@ -169,60 +172,62 @@
</template> </template>
<template v-else> <template v-else>
<a-form-item <a-form-item
field="taskName" field="name"
:label="t('apiTestManagement.taskName')" :label="t('apiTestManagement.taskName')"
:rules="[{ required: true, message: t('apiTestManagement.taskNameRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.taskNameRequired') }]"
> >
<div class="flex w-full items-center gap-[8px]"> <div class="flex w-full items-center gap-[8px]">
<a-input <a-input
v-model:model-value="importForm.taskName" v-model:model-value="importForm.name"
:placeholder="t('apiTestManagement.taskNamePlaceholder')" :placeholder="t('apiTestManagement.taskNamePlaceholder')"
:max-length="255" :max-length="255"
class="flex-1" class="flex-1"
></a-input> ></a-input>
<MsButton type="text">{{ t('apiTestManagement.timeTaskList') }}</MsButton> <MsButton type="text" @click="taskDrawerVisible = true">{{
t('apiTestManagement.timeTaskList')
}}</MsButton>
</div> </div>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="url" field="swaggerUrl"
label="SwaggerURL" label="SwaggerURL"
asterisk-position="end" asterisk-position="end"
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
> >
<a-input <a-input
v-model:model-value="importForm.url" v-model:model-value="importForm.swaggerUrl"
:placeholder="t('apiTestManagement.urlImportPlaceholder')" :placeholder="t('apiTestManagement.urlImportPlaceholder')"
class="w-[700px]" class="w-[700px]"
allow-clear allow-clear
></a-input> ></a-input>
</a-form-item> </a-form-item>
<div class="mb-[16px] flex items-center gap-[8px]"> <div class="mb-[16px] flex items-center gap-[8px]">
<a-switch v-model:model-value="importForm.basicAuth" type="line" size="small"></a-switch> <a-switch v-model:model-value="importForm.authSwitch" type="line" size="small"></a-switch>
{{ t('apiTestManagement.basicAuth') }} {{ t('apiTestManagement.basicAuth') }}
</div> </div>
<template v-if="importForm.basicAuth"> <template v-if="importForm.authSwitch">
<a-form-item <a-form-item
field="account" field="authUsername"
:label="t('apiTestManagement.account')" :label="t('apiTestManagement.account')"
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
asterisk-position="end" asterisk-position="end"
> >
<a-input <a-input
v-model:model-value="importForm.account" v-model:model-value="importForm.authUsername"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
class="w-[500px]" class="w-[500px]"
allow-clear allow-clear
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="password" field="authPassword"
:label="t('apiTestManagement.password')" :label="t('apiTestManagement.password')"
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
asterisk-position="end" asterisk-position="end"
autocomplete="new-password" autocomplete="new-password"
> >
<a-input-password <a-input-password
v-model:model-value="importForm.password" v-model:model-value="importForm.authPassword"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
class="w-[500px]" class="w-[500px]"
autocomplete="new-password" autocomplete="new-password"
@ -232,7 +237,7 @@
</template> </template>
<a-form-item :label="t('apiTestManagement.belongModule')"> <a-form-item :label="t('apiTestManagement.belongModule')">
<a-tree-select <a-tree-select
v-model:modelValue="importForm.module" v-model:modelValue="importForm.moduleId"
:data="moduleTree" :data="moduleTree"
class="w-[436px]" class="w-[436px]"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :field-names="{ title: 'name', key: 'id', children: 'children' }"
@ -261,13 +266,13 @@
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-select v-model:model-value="importForm.mode" class="w-[240px]"> <a-select v-model:model-value="importForm.coverData" class="w-[240px]">
<a-option value="cover">{{ t('apiTestManagement.cover') }}</a-option> <a-option :value="true">{{ t('apiTestManagement.cover') }}</a-option>
<a-option value="uncover">{{ t('apiTestManagement.uncover') }}</a-option> <a-option :value="false">{{ t('apiTestManagement.uncover') }}</a-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item :label="t('apiTestManagement.syncFrequency')"> <a-form-item :label="t('apiTestManagement.syncFrequency')">
<a-select v-model:model-value="importForm.syncFrequency" class="w-[240px]"> <a-select v-model:model-value="cronValue" class="w-[240px]">
<template #label="{ data }"> <template #label="{ data }">
<div class="flex items-center"> <div class="flex items-center">
{{ data.value }} {{ data.value }}
@ -280,46 +285,88 @@
<div class="ml-[4px] text-[var(--color-text-4)]">{{ item.label }}</div> <div class="ml-[4px] text-[var(--color-text-4)]">{{ item.label }}</div>
</div> </div>
</a-option> </a-option>
<template #footer> <!-- TODO:第一版不做自定义 -->
<!-- <template #footer>
<div class="flex items-center p-[4px_8px]"> <div class="flex items-center p-[4px_8px]">
<MsButton type="text">{{ t('apiTestManagement.customFrequency') }}</MsButton> <MsButton type="text">{{ t('apiTestManagement.customFrequency') }}</MsButton>
</div> </div>
</template> </template> -->
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>
</MsDrawer> </MsDrawer>
<MsDrawer v-model:visible="taskDrawerVisible" :width="960" :title="t('apiTestManagement.timeTask')" :footer="false">
<div class="mb-[16px] flex items-center justify-between">
{{ t('apiTestManagement.timeTaskList') }}
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiTestManagement.searchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadTaskList"
@press-enter="loadTaskList"
/>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #action="{ record }">
<a-switch
v-model:modelValue="record.enable"
type="line"
size="small"
:before-change="() => handleBeforeEnableChange(record)"
></a-switch>
</template>
</ms-base-table>
</MsDrawer>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormInstance, Message } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsUpload from '@/components/pure/ms-upload/index.vue'; import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import {
createDefinitionSchedule,
importDefinition,
switchDefinitionSchedule,
} from '@/api/modules/api-test/management';
import { getScheduleProApiCaseList } from '@/api/modules/project-management/taskCenter';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { mapTree } from '@/utils'; import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common'; import type { ImportApiDefinitionParams, ImportApiDefinitionRequest } from '@/models/apiTest/management';
import { RequestImportFormat } from '@/enums/apiEnum'; import type { ModuleTreeNode } from '@/models/common';
import { TimingTaskCenterApiCaseItem } from '@/models/projectManagement/taskCenter';
import { RequestImportFormat, RequestImportType } from '@/enums/apiEnum';
import { TaskCenterEnum } from '@/enums/taskCenter';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
moduleTree: ModuleTreeNode[]; moduleTree: ModuleTreeNode[];
popupContainer?: string; popupContainer?: string;
}>(); }>();
const emit = defineEmits(['update:visible']); const emit = defineEmits(['update:visible', 'done']);
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore();
const userStore = useUserStore();
const visible = useVModel(props, 'visible', emit); const visible = useVModel(props, 'visible', emit);
const importType = ref<'file' | 'time'>('file'); const importType = ref<'file' | 'time'>('file');
const importFormat = ref<keyof typeof RequestImportFormat>('SWAGGER'); const platformList = [
const importFormatList = [
{ {
name: 'Swagger', name: 'Swagger',
value: RequestImportFormat.SWAGGER, value: RequestImportFormat.SWAGGER,
@ -327,64 +374,232 @@
iconColor: 'rgb(var(--success-7))', iconColor: 'rgb(var(--success-7))',
}, },
]; ];
const fileList = ref<MsFileItem[]>([]);
function setActiveImportFormat(format: RequestImportFormat) { const defaultForm: ImportApiDefinitionRequest = {
importFormat.value = format; platform: RequestImportFormat.SWAGGER,
} name: '',
moduleId: 'root',
const defaultForm = { coverData: true,
taskName: '', syncCase: true,
module: 'root', coverModule: false,
mode: 'cover', swaggerUrl: '',
syncImportCase: true, authSwitch: false,
syncUpdateDirectory: false, authUsername: '',
importType: 'file', authPassword: '',
file: [], type: RequestImportType.API,
url: '', userId: userStore.id || '',
basicAuth: false, protocol: 'HTTP',
account: '', projectId: appStore.currentProjectId,
password: '',
syncFrequency: '0 0 0/1 * ?',
}; };
const importForm = ref({ ...defaultForm }); const importForm = ref({ ...defaultForm });
const importFormRef = ref<FormInstance>(); const importFormRef = ref<FormInstance>();
const moreSettingActive = ref<number[]>([]); const moreSettingActive = ref<number[]>([]);
const disabledConfirm = computed(() => { const disabledConfirm = computed(() => {
if (importForm.value.type === RequestImportType.API) {
if (importType.value === 'file') { if (importType.value === 'file') {
if (importForm.value.importType === 'file') { return !fileList.value.length;
return !importForm.value.file.length;
} }
return !importForm.value.url; return !importForm.value.swaggerUrl;
} }
return !importForm.value.taskName || !importForm.value.url; return !importForm.value.name || !importForm.value.swaggerUrl;
}); });
const moduleTree = computed(() => mapTree(props.moduleTree, (node) => ({ ...node, draggable: false }))); const moduleTree = computed(() => mapTree(props.moduleTree, (node) => ({ ...node, draggable: false })));
const syncFrequencyOptions = [ const syncFrequencyOptions = [
{ label: t('apiTestManagement.timeTaskHour'), value: '0 0 0/1 * ?' }, { label: t('apiTestManagement.timeTaskHour'), value: '0 0 0/1 * * ? ' },
{ label: t('apiTestManagement.timeTaskSixHour'), value: '0 0 0/6 * ?' }, { label: t('apiTestManagement.timeTaskSixHour'), value: '0 0 0/6 * * ?' },
{ label: t('apiTestManagement.timeTaskTwelveHour'), value: '0 0 0/12 * ?' }, { label: t('apiTestManagement.timeTaskTwelveHour'), value: '0 0 0/12 * * ?' },
{ label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * ?' }, { label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * * ?' },
]; ];
const cronValue = ref('0 0 0/1 * * ? ');
const importLoading = ref(false);
const taskDrawerVisible = ref(false);
function setActiveImportFormat(format: RequestImportFormat) {
importForm.value.platform = format;
}
function cancelImport() { function cancelImport() {
visible.value = false; visible.value = false;
importForm.value = { ...defaultForm }; importForm.value = { ...defaultForm };
importFormRef.value?.resetFields(); importFormRef.value?.resetFields();
importType.value = 'file';
fileList.value = [];
moreSettingActive.value = [];
} }
function confirmImport() { async function importDefinitionByFile() {
importFormRef.value?.validate(async (errors) => {
if (!errors) {
try { try {
importLoading.value = true;
let params: ImportApiDefinitionParams;
if (importType.value === 'file') {
params = {
file: fileList.value[0].file || null,
request: {
type: importForm.value.type,
platform: importForm.value.platform,
userId: userStore.id || '',
projectId: appStore.currentProjectId,
coverModule: importForm.value.coverModule,
coverData: importForm.value.coverData,
syncCase: importForm.value.syncCase,
protocol: importForm.value.protocol,
moduleId: importForm.value.moduleId,
authSwitch: importForm.value.authSwitch,
},
};
} else {
params = {
file: null,
request: {
type: importForm.value.type,
platform: importForm.value.platform,
userId: userStore.id || '',
projectId: appStore.currentProjectId,
coverModule: importForm.value.coverModule,
coverData: importForm.value.coverData,
syncCase: importForm.value.syncCase,
protocol: importForm.value.protocol,
moduleId: importForm.value.moduleId,
swaggerUrl: importForm.value.swaggerUrl,
authSwitch: importForm.value.authSwitch,
authUsername: importForm.value.authUsername,
authPassword: importForm.value.authPassword,
},
};
}
await importDefinition(params);
Message.success(t('common.importSuccess')); Message.success(t('common.importSuccess'));
emit('done');
cancelImport(); cancelImport();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
importLoading.value = false;
}
}
async function importDefinitionBySchedule() {
try {
importLoading.value = true;
await createDefinitionSchedule({
type: importForm.value.type,
platform: importForm.value.platform,
userId: userStore.id || '',
projectId: appStore.currentProjectId,
coverModule: importForm.value.coverModule,
coverData: importForm.value.coverData,
syncCase: importForm.value.syncCase,
protocol: importForm.value.protocol,
moduleId: importForm.value.moduleId,
swaggerUrl: importForm.value.swaggerUrl,
authSwitch: importForm.value.authSwitch,
authUsername: importForm.value.authUsername,
authPassword: importForm.value.authPassword,
value: cronValue.value,
name: importForm.value.name,
});
Message.success(t('apiTestManagement.createTaskSuccess'));
taskDrawerVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
importLoading.value = false;
}
}
function confirmImport() {
importFormRef.value?.validate((errors) => {
if (!errors) {
if (importForm.value.type === RequestImportType.API) {
importDefinitionByFile();
} else {
importDefinitionBySchedule();
} }
} }
}); });
} }
const keyword = ref('');
const columns: MsTableColumn = [
{
title: 'apiTestManagement.name',
dataIndex: 'taskName',
showTooltip: true,
width: 150,
},
{
title: 'apiTestManagement.taskRunRule',
dataIndex: 'value',
width: 140,
},
{
title: 'apiTestManagement.taskNextRunTime',
dataIndex: 'nextTime',
showTooltip: true,
width: 180,
},
{
title: 'apiTestManagement.taskOperator',
dataIndex: 'createUserName',
showTooltip: true,
width: 150,
},
{
title: 'apiTestManagement.taskOperationTime',
dataIndex: 'createTime',
width: 180,
},
{
title: 'common.operation',
slotName: 'action',
dataIndex: 'operation',
fixed: 'right',
width: 80,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getScheduleProApiCaseList,
{
columns,
scroll: { x: '100%' },
},
(item) => ({
...item,
operationTime: dayjs(item.operationTime).format('YYYY-MM-DD HH:mm:ss'),
})
);
function loadTaskList() {
setLoadListParams({
keyword: keyword.value,
moduleType: TaskCenterEnum.API_IMPORT,
});
loadList();
}
watch(
() => taskDrawerVisible.value,
(value) => {
if (value) {
loadTaskList();
}
}
);
async function handleBeforeEnableChange(record: TimingTaskCenterApiCaseItem) {
try {
await switchDefinitionSchedule(record.id);
Message.success(
t(record.enable ? 'apiTestManagement.disableTaskSuccess' : 'apiTestManagement.enableTaskSuccess')
);
return true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
return false;
}
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,10 +1,6 @@
<template> <template>
<div :class="['p-[16px_22px]', props.class]"> <div :class="['p-[16px_22px]', props.class]">
<div class="mb-[16px] flex items-center justify-between"> <div class="mb-[16px] flex items-center justify-between">
<div v-if="!props.readOnly" class="flex items-center gap-[8px]">
<a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch>
{{ t('apiTestManagement.showSubdirectory') }}
</div>
<div class="flex items-center gap-[8px]"> <div class="flex items-center gap-[8px]">
<a-input-search <a-input-search
v-model:model-value="keyword" v-model:model-value="keyword"
@ -37,7 +33,7 @@
trigger="click" trigger="click"
@popup-visible-change="handleFilterHidden" @popup-visible-change="handleFilterHidden"
> >
<MsButton type="text" class="arco-btn-text--secondary" @click="methodFilterVisible = true"> <MsButton type="text" class="arco-btn-text--secondary ml-[10px]" @click="methodFilterVisible = true">
{{ t(columnConfig.title as string) }} {{ t(columnConfig.title as string) }}
<icon-down :class="methodFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" /> <icon-down :class="methodFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</MsButton> </MsButton>
@ -81,12 +77,22 @@
<MsButton type="text" @click="openApiTab(record)">{{ record.num }}</MsButton> <MsButton type="text" @click="openApiTab(record)">{{ record.num }}</MsButton>
</template> </template>
<template #method="{ record }"> <template #method="{ record }">
<a-select
v-model:model-value="record.method"
class="param-input w-full"
@change="() => handleMethodChange(record)"
>
<template #label>
<apiMethodName :method="record.method" is-tag /> <apiMethodName :method="record.method" is-tag />
</template> </template>
<a-option v-for="item of Object.values(RequestMethods)" :key="item" :value="item">
<apiMethodName :method="item" is-tag />
</a-option>
</a-select>
</template>
<template #status="{ record }"> <template #status="{ record }">
<a-select <a-select
v-model:model-value="record.status" v-model:model-value="record.status"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full" class="param-input w-full"
@change="() => handleStatusChange(record)" @change="() => handleStatusChange(record)"
> >
@ -103,7 +109,7 @@
{{ t('apiTestManagement.execute') }} {{ t('apiTestManagement.execute') }}
</MsButton> </MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
<MsButton type="text" class="!mr-0"> <MsButton type="text" class="!mr-0" @click="copyDefinition(record)">
{{ t('common.copy') }} {{ t('common.copy') }}
</MsButton> </MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
@ -136,12 +142,14 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
v-if="batchForm.attr === 'tag'" v-if="batchForm.attr === 'tags'"
field="values" field="values"
:label="t('apiTestManagement.batchUpdate')" :label="t('apiTestManagement.batchUpdate')"
:validate-trigger="['blur', 'input']"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]" :rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end" asterisk-position="end"
class="mb-0" class="mb-0"
required
> >
<MsTagsInput <MsTagsInput
v-model:model-value="batchForm.values" v-model:model-value="batchForm.values"
@ -159,11 +167,7 @@
asterisk-position="end" asterisk-position="end"
class="mb-0" class="mb-0"
> >
<apiMethodSelect <apiMethodSelect v-if="batchForm.attr === 'method'" v-model:model-value="batchForm.value" />
v-if="batchForm.attr === 'type'"
v-model:model-value="batchForm.value"
@change="handleActiveDebugChange"
/>
<a-select <a-select
v-else v-else
v-model="batchForm.value" v-model="batchForm.value"
@ -218,7 +222,7 @@
<moduleTree <moduleTree
v-if="moveModalVisible" v-if="moveModalVisible"
:is-expand-all="true" :is-expand-all="true"
is-modal :is-modal="true"
:active-module="props.activeModule" :active-module="props.activeModule"
@folder-node-select="folderNodeSelect" @folder-node-select="folderNodeSelect"
/> />
@ -241,7 +245,14 @@
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import moduleTree from '@/views/api-test/management/components/moduleTree.vue'; import moduleTree from '@/views/api-test/management/components/moduleTree.vue';
import { deleteDefinition, getDefinitionPage } from '@/api/modules/api-test/management'; import {
batchDeleteDefinition,
batchMoveDefinition,
batchUpdateDefinition,
deleteDefinition,
getDefinitionPage,
updateDefinition,
} from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
@ -259,39 +270,16 @@
readOnly?: boolean; // readOnly?: boolean; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'init', params: any): void;
(e: 'change'): void;
(e: 'openApiTab', record: ApiDefinitionDetail): void; (e: 'openApiTab', record: ApiDefinitionDetail): void;
(e: 'openCopyApiTab', record: ApiDefinitionDetail): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
function handleActiveDebugChange() { const folderTreePathMap = inject('folderTreePathMap');
emit('change'); const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
}
const showSubdirectory = ref(false);
const checkedEnv = ref('DEV');
const envOptions = ref([
{
label: 'DEV',
value: 'DEV',
},
{
label: 'TEST',
value: 'TEST',
},
{
label: 'PRE',
value: 'PRE',
},
{
label: 'PROD',
value: 'PROD',
},
]);
const keyword = ref(''); const keyword = ref('');
let columns: MsTableColumn = [ let columns: MsTableColumn = [
@ -322,7 +310,7 @@
dataIndex: 'method', dataIndex: 'method',
slotName: 'method', slotName: 'method',
titleSlotName: 'methodFilter', titleSlotName: 'methodFilter',
width: 120, width: 140,
}, },
{ {
title: 'apiTestManagement.apiStatus', title: 'apiTestManagement.apiStatus',
@ -333,7 +321,6 @@
}, },
{ {
title: 'apiTestManagement.path', title: 'apiTestManagement.path',
slotName: 'path',
dataIndex: 'path', dataIndex: 'path',
showTooltip: true, showTooltip: true,
width: 200, width: 200,
@ -386,19 +373,21 @@
selectable: true, selectable: true,
showSelectAll: !props.readOnly, showSelectAll: !props.readOnly,
draggable: props.readOnly ? undefined : { type: 'handle', width: 32 }, draggable: props.readOnly ? undefined : { type: 'handle', width: 32 },
heightUsed: 374,
}, },
(item) => ({ (item) => ({
...item, ...item,
fullPath: folderTreePathMap?.[item.moduleId],
createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'), createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'), updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
}) })
); );
const batchActions = { const batchActions = {
baseAction: [ baseAction: [
{ // {
label: 'common.export', // label: 'common.export',
eventTag: 'export', // eventTag: 'export',
}, // },
{ {
label: 'common.edit', label: 'common.edit',
eventTag: 'edit', eventTag: 'edit',
@ -432,10 +421,6 @@
if (props.activeModule === 'all') { if (props.activeModule === 'all') {
return []; return [];
} }
if (showSubdirectory.value) {
//
return [props.activeModule, ...props.offspringIds];
}
return [props.activeModule]; return [props.activeModule];
}); });
const tableQueryParams = ref<any>(); const tableQueryParams = ref<any>();
@ -444,9 +429,8 @@
keyword: keyword.value, keyword: keyword.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: moduleIds.value, moduleIds: moduleIds.value,
env: checkedEnv.value,
protocol: props.protocol, protocol: props.protocol,
filter: { status: statusFilters.value, type: methodFilters.value }, filter: { status: statusFilters.value, method: methodFilters.value },
}; };
setLoadListParams(params); setLoadListParams(params);
loadList(); loadList();
@ -455,14 +439,12 @@
current: propsRes.value.msPagination?.current, current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize, pageSize: propsRes.value.msPagination?.pageSize,
}; };
emit('init', {
...tableQueryParams.value,
});
} }
watch( watch(
() => props.activeModule, () => props.activeModule,
() => { () => {
resetSelector();
loadApiList(); loadApiList();
} }
); );
@ -470,6 +452,7 @@
watch( watch(
() => props.protocol, () => props.protocol,
() => { () => {
resetSelector();
loadApiList(); loadApiList();
} }
); );
@ -480,8 +463,25 @@
} }
} }
async function handleMethodChange(record: ApiDefinitionDetail) {
try {
await updateDefinition({
id: record.id,
method: record.method,
});
Message.success(t('common.updateSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function handleStatusChange(record: ApiDefinitionDetail) { async function handleStatusChange(record: ApiDefinitionDetail) {
try { try {
await updateDefinition({
id: record.id,
status: record.status,
});
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -493,16 +493,6 @@
loadApiList(); loadApiList();
}); });
function emitTableParams() {
emit('init', {
keyword: keyword.value,
moduleIds: [],
projectId: appStore.currentProjectId,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
const tableSelected = ref<(string | number)[]>([]); const tableSelected = ref<(string | number)[]>([]);
const batchParams = ref<BatchActionQueryParams>({ const batchParams = ref<BatchActionQueryParams>({
selectedIds: [], selectedIds: [],
@ -536,20 +526,25 @@
onBeforeOk: async () => { onBeforeOk: async () => {
try { try {
if (isBatch) { if (isBatch) {
// await batchDeleteDefinition({ await batchDeleteDefinition({
// selectIds, selectIds,
// selectAll: !!params?.selectAll, selectAll: !!params?.selectAll,
// excludeIds: params?.excludeIds || [], excludeIds: params?.excludeIds || [],
// condition: { keyword: keyword.value }, condition: { keyword: keyword.value },
// projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
// moduleIds: props.activeModule === 'all' ? [] : [props.activeModule], moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
// }); deleteAll: true,
protocol: props.protocol,
});
} else { } else {
await deleteDefinition(record?.id as string); await deleteDefinition(record?.id as string);
} }
Message.success(t('common.deleteSuccess')); Message.success(t('common.deleteSuccess'));
resetSelector(); resetSelector();
loadList(); loadList();
if (typeof refreshModuleTree === 'function') {
refreshModuleTree();
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -588,20 +583,26 @@
value: '', value: '',
values: [], values: [],
}); });
const attrOptions = [ const fullAttrs = [
{ {
name: 'apiTestManagement.apiStatus', name: 'apiTestManagement.apiStatus',
value: 'status', value: 'status',
}, },
{ {
name: 'apiTestManagement.apiType', name: 'apiTestManagement.apiType',
value: 'type', value: 'method',
}, },
{ {
name: 'common.tag', name: 'common.tag',
value: 'tag', value: 'tags',
}, },
]; ];
const attrOptions = computed(() => {
if (props.protocol === 'HTTP') {
return fullAttrs;
}
return fullAttrs.filter((e) => e.value !== 'method');
});
const valueOptions = computed(() => { const valueOptions = computed(() => {
switch (batchForm.value.attr) { switch (batchForm.value.attr) {
case 'status': case 'status':
@ -643,6 +644,17 @@
if (!errors) { if (!errors) {
try { try {
batchUpdateLoading.value = true; batchUpdateLoading.value = true;
await batchUpdateDefinition({
selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [],
condition: { keyword: keyword.value },
projectId: appStore.currentProjectId,
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
protocol: props.protocol,
type: batchForm.value.attr,
[batchForm.value.attr]: batchForm.value.attr === 'tags' ? batchForm.value.values : batchForm.value.value,
});
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
cancelBatch(); cancelBatch();
resetSelector(); resetSelector();
@ -669,16 +681,17 @@
async function handleApiMove() { async function handleApiMove() {
try { try {
batchMoveApiLoading.value = true; batchMoveApiLoading.value = true;
// await batchMoveFile({ await batchMoveDefinition({
// selectIds: isBatchMove.value ? batchParams.value?.selectedIds || [] : [activeApi.value?.id || ''], selectIds: isBatchMove.value ? batchParams.value?.selectedIds || [] : [activeApi.value?.id || ''],
// selectAll: !!batchParams.value?.selectAll, selectAll: !!batchParams.value?.selectAll,
// excludeIds: batchParams.value?.excludeIds || [], excludeIds: batchParams.value?.excludeIds || [],
// condition: { keyword: keyword.value }, condition: { keyword: keyword.value },
// projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
// moduleIds: props.activeModule === 'all' ? [] : [props.activeModule], moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
// moveModuleId: selectedModuleKeys.value[0], moduleId: selectedModuleKeys.value[0],
// }); protocol: props.protocol,
Message.success(t('apiTestManagement.batchMoveSuccess')); });
Message.success(t('common.batchMoveSuccess'));
if (isBatchMove.value) { if (isBatchMove.value) {
tableSelected.value = []; tableSelected.value = [];
isBatchMove.value = false; isBatchMove.value = false;
@ -687,7 +700,9 @@
} }
loadList(); loadList();
resetSelector(); resetSelector();
emitTableParams(); if (typeof refreshModuleTree === 'function') {
refreshModuleTree();
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -735,6 +750,10 @@
emit('openApiTab', record); emit('openApiTab', record);
} }
function copyDefinition(record: ApiDefinitionDetail) {
emit('openCopyApiTab', record);
}
defineExpose({ defineExpose({
loadApiList, loadApiList,
}); });

View File

@ -28,6 +28,7 @@
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:protocol="props.protocol" :protocol="props.protocol"
@open-api-tab="openApiTab" @open-api-tab="openApiTab"
@open-copy-api-tab="openApiTab($event, true)"
/> />
</div> </div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden"> <div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
@ -49,7 +50,7 @@
hide-response-layout-switch hide-response-layout-switch
:create-api="addDefinition" :create-api="addDefinition"
:update-api="updateDefinition" :update-api="updateDefinition"
:execute-api="executeDebug" :execute-api="debugDefinition"
:local-execute-api="localExecuteApiDebug" :local-execute-api="localExecuteApiDebug"
:permission-map="{ :permission-map="{
execute: 'PROJECT_API_DEFINITION:READ+EXECUTE', execute: 'PROJECT_API_DEFINITION:READ+EXECUTE',
@ -196,9 +197,10 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { executeDebug, localExecuteApiDebug } from '@/api/modules/api-test/debug'; import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { import {
addDefinition, addDefinition,
debugDefinition,
getDefinitionDetail, getDefinitionDetail,
getTransferOptions, getTransferOptions,
transferFile, transferFile,
@ -411,9 +413,9 @@
} }
const loading = ref(false); const loading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail) { async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail, isCopy = false) {
const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id); const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id);
if (isLoadedTabIndex > -1) { if (isLoadedTabIndex > -1 && !isCopy) {
// tabtab // tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam; activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
return; return;
@ -421,14 +423,17 @@
try { try {
loading.value = true; loading.value = true;
const res = await getDefinitionDetail(apiInfo.id); const res = await getDefinitionDetail(apiInfo.id);
const name = isCopy ? `${res.name}-copy` : res.name;
addApiTab({ addApiTab({
label: apiInfo.name, label: name,
...res.request, ...res.request,
...res, ...res,
response: cloneDeep(defaultResponse), response: cloneDeep(defaultResponse),
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
url: res.path, url: res.path,
name: res.name, // requestnamenull name, // requestnamenull
isNew: false, isNew: isCopy,
unSaved: isCopy,
}); });
nextTick(() => { nextTick(() => {
// loading // loading
@ -483,7 +488,7 @@
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>(); const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const activeApiTabFormRef = ref<FormInstance>(); const activeApiTabFormRef = ref<FormInstance>();
function handleSave(params: ApiDefinitionCreateParams | ApiDefinitionUpdateParams) { function handleSave(params: ApiDefinitionCreateParams) {
activeApiTabFormRef.value?.validate(async (errors) => { activeApiTabFormRef.value?.validate(async (errors) => {
if (errors) { if (errors) {
splitBoxRef.value?.expand(); splitBoxRef.value?.expand();
@ -521,9 +526,14 @@
console.log(params); console.log(params);
} }
function refreshTable() {
apiTableRef.value?.loadApiList();
}
defineExpose({ defineExpose({
openApiTab, openApiTab,
addApiTab, addApiTab,
refreshTable,
}); });
</script> </script>

View File

@ -28,12 +28,14 @@
</template> </template>
</a-button> </a-button>
<MsSelect <MsSelect
v-model:model-value="checkedEnv" v-model:model-value="currentEnv"
mode="static" mode="static"
:options="envOptions" :options="envOptions"
class="!w-[150px]" class="!w-[150px]"
:search-keys="['label']" :search-keys="['label']"
:loading="envLoading"
allow-search allow-search
@change="initEnvironment"
/> />
</div> </div>
</template> </template>
@ -41,11 +43,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import api from './api/index.vue'; import api from './api/index.vue';
import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue'; import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { useI18n } from '@/hooks/useI18n'; import { getEnvironment, getEnvList } from '@/api/modules/api-test/common';
import useAppStore from '@/store/modules/app';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
@ -56,7 +61,7 @@
moduleTree: ModuleTreeNode[]; // moduleTree: ModuleTreeNode[]; //
}>(); }>();
const { t } = useI18n(); const appStore = useAppStore();
const activeTab = ref('api'); const activeTab = ref('api');
const apiRef = ref<InstanceType<typeof api>>(); const apiRef = ref<InstanceType<typeof api>>();
@ -69,28 +74,52 @@
} }
} }
const checkedEnv = ref('DEV'); const currentEnv = ref('');
const envOptions = ref([ const currentEnvConfig = ref({});
{ const envLoading = ref(false);
label: 'DEV', const envOptions = ref<SelectOptionData[]>([]);
value: 'DEV',
}, async function initEnvironment() {
{ try {
label: 'TEST', currentEnvConfig.value = await getEnvironment(currentEnv.value);
value: 'TEST', } catch (error) {
}, // eslint-disable-next-line no-console
{ console.log(error);
label: 'PRE', }
value: 'PRE', }
},
{ async function initEnvList() {
label: 'PROD', try {
value: 'PROD', envLoading.value = true;
}, const res = await getEnvList(appStore.currentProjectId);
]); envOptions.value = res.map((item) => ({
label: item.name,
value: item.id,
}));
currentEnv.value = res[0]?.id || '';
initEnvironment();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
envLoading.value = false;
}
}
function refreshApiTable() {
apiRef.value?.refreshTable();
}
onBeforeMount(() => {
initEnvList();
});
/** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig));
defineExpose({ defineExpose({
newTab, newTab,
refreshApiTable,
}); });
</script> </script>

View File

@ -1,5 +1,6 @@
<template> <template>
<div> <div>
<template v-if="!props.isModal">
<a-select <a-select
v-if="!props.readOnly" v-if="!props.readOnly"
v-model:model-value="moduleProtocol" v-model:model-value="moduleProtocol"
@ -13,7 +14,9 @@
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button> <a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
<template #content> <template #content>
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption> <a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption> <a-doption v-if="moduleProtocol === 'HTTP'" value="import">
{{ t('apiTestManagement.importApi') }}
</a-doption>
</template> </template>
</a-dropdown> </a-dropdown>
</div> </div>
@ -64,6 +67,14 @@
</div> </div>
</div> </div>
<a-divider class="my-[8px]" /> <a-divider class="my-[8px]" />
</template>
<a-input
v-else
v-model:model-value="moduleKeyword"
:placeholder="t('apiTestManagement.searchTip')"
class="mb-[16px]"
allow-clear
/>
<a-spin class="min-h-[400px] w-full" :loading="loading"> <a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree <MsTree
v-model:focus-node-key="focusNodeKey" v-model:focus-node-key="focusNodeKey"
@ -81,8 +92,9 @@
children: 'children', children: 'children',
count: 'count', count: 'count',
}" }"
:draggable="!props.readOnly" :draggable="!props.readOnly && !props.isModal"
:filter-more-action-func="filterMoreActionFunc" :filter-more-action-func="filterMoreActionFunc"
:allow-drop="allowDrop"
block-node block-node
title-tooltip-position="left" title-tooltip-position="left"
@select="folderNodeSelect" @select="folderNodeSelect"
@ -99,12 +111,12 @@
<apiMethodName :method="nodeData.attachInfo?.method || nodeData.attachInfo?.protocol" /> <apiMethodName :method="nodeData.attachInfo?.method || nodeData.attachInfo?.protocol" />
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div> <div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
</div> </div>
<div v-else class="inline-flex w-full"> <div v-else :id="nodeData.id" class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div> <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)]">({{ nodeData.count || 0 }})</div> <div v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div> </div>
</template> </template>
<template v-if="!props.readOnly" #extra="nodeData"> <template v-if="!props.readOnly && !props.isModal" #extra="nodeData">
<!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 --> <!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 -->
<popConfirm <popConfirm
v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'" v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'"
@ -122,12 +134,13 @@
<popConfirm <popConfirm
v-if="nodeData.id !== 'root'" v-if="nodeData.id !== 'root'"
mode="rename" mode="rename"
:node-type="nodeData.type"
:parent-id="nodeData.id" :parent-id="nodeData.id"
:node-id="nodeData.id" :node-id="nodeData.id"
:field-config="{ field: renameFolderTitle }" :field-config="{ field: renameFolderTitle }"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')" :all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:update-module-api="updateModule" :update-module-api="updateModule"
:update-api-node-api="updateModule" :update-api-node-api="updateDefinition"
@close="resetFocusNodeKey" @close="resetFocusNodeKey"
@rename-finish="initModules" @rename-finish="initModules"
> >
@ -159,8 +172,11 @@
getModuleTree, getModuleTree,
getModuleTreeOnlyModules, getModuleTreeOnlyModules,
moveModule, moveModule,
sortDefinition,
updateDefinition,
updateModule, updateModule,
} from '@/api/modules/api-test/management'; } from '@/api/modules/api-test/management';
import { dropPositionMap } from '@/config/common';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -174,9 +190,12 @@
activeModule?: string | number; // key activeModule?: string | number; // key
readOnly?: boolean; // readOnly?: boolean; //
activeNodeId?: string | number; // id activeNodeId?: string | number; // id
isModal?: boolean; //
}>(), }>(),
{ {
activeModule: 'all', activeModule: 'all',
readOnly: false,
isModal: false,
} }
); );
const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode', 'changeProtocol']); const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode', 'changeProtocol']);
@ -224,7 +243,7 @@
} }
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
if (props.readOnly) { if (props.readOnly || props.isModal) {
return { return {
height: 'calc(60vh - 190px)', height: 'calc(60vh - 190px)',
threshold: 200, threshold: 200,
@ -335,28 +354,38 @@
moduleIds: [], moduleIds: [],
}); });
} }
if (props.readOnly) { const nodePathObj: Record<string, any> = {};
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => { if (props.readOnly || props.isModal) {
folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
//
nodePathObj[e.id] = {
path: e.path,
fullPath,
};
return { return {
...e, ...e,
hideMoreAction: true, hideMoreAction: true,
draggable: false, draggable: false,
disabled: e.id === selectedKeys.value[0],
}; };
}); });
} else { } else {
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => { folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
//
nodePathObj[e.id] = {
path: e.path,
fullPath,
};
return { return {
...e, ...e,
hideMoreAction: e.id === 'root', hideMoreAction: e.id === 'root',
draggable: e.id !== 'root',
disabled: e.id === selectedKeys.value[0],
}; };
}); });
} }
if (isSetDefaultKey) { if (isSetDefaultKey) {
selectedKeys.value = [folderTree.value[0].id]; selectedKeys.value = [folderTree.value[0].id];
} }
emit('init', folderTree.value, moduleProtocol.value); emit('init', folderTree.value, moduleProtocol.value, nodePathObj);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -378,7 +407,8 @@
return { return {
...node, ...node,
count: res[node.id] || 0, count: res[node.id] || 0,
draggable: props.readOnly ? false : node.id !== 'root', draggable: !(props.readOnly || props.isModal),
disabled: props.readOnly || props.isModal ? node.id === selectedKeys.value[0] : false,
}; };
}); });
} catch (error) { } catch (error) {
@ -482,6 +512,19 @@
} }
} }
function allowDrop(dropNode: MsTreeNodeData, dropPosition: number, dragNode?: MsTreeNodeData | null) {
if (dropNode.type === 'API' && dropPosition === 0) {
// API
return false;
}
if (dropNode.type === 'MODULE' && dragNode?.type === 'API' && dropPosition !== 0) {
// API
document.querySelector('.arco-tree-node-title-draggable::before')?.setAttribute('style', 'display: none');
return false;
}
return true;
}
/** /**
* 处理文件夹树节点拖拽事件 * 处理文件夹树节点拖拽事件
* @param tree 树数据 * @param tree 树数据
@ -495,13 +538,27 @@
dropNode: MsTreeNodeData, dropNode: MsTreeNodeData,
dropPosition: number dropPosition: number
) { ) {
if (dragNode.id === 'root' || (dragNode.type === 'MODULE' && dropNode.id === 'root')) {
//
return;
}
try { try {
loading.value = true; loading.value = true;
if (dragNode.type === 'MODULE') {
await moveModule({ await moveModule({
dragNodeId: dragNode.id as string, dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '', dropNodeId: dropNode.id || '',
dropPosition, dropPosition,
}); });
} else {
await sortDefinition({
projectId: appStore.currentProjectId,
moveMode: dropPositionMap[dropPosition],
moveId: dragNode.id,
targetId: dropNode.id,
moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id, // APIidid
});
}
Message.success(t('apiTestDebug.moduleMoveSuccess')); Message.success(t('apiTestDebug.moduleMoveSuccess'));
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -567,4 +624,7 @@
} }
} }
} }
:deep(#root ~ .arco-tree-node-drag-icon) {
@apply hidden;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<MsCard simple no-content-padding> <MsCard :min-width="1180" simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5"> <MsSplitBox :size="0.25" :max="0.5">
<template #first> <template #first>
<div class="p-[24px]"> <div class="p-[24px]">
@ -36,6 +36,7 @@
v-model:visible="importDrawerVisible" v-model:visible="importDrawerVisible"
:module-tree="folderTree" :module-tree="folderTree"
popup-container="#managementContainer" popup-container="#managementContainer"
@done="handleImportDone"
/> />
</div> </div>
<management <management
@ -65,6 +66,7 @@
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 importDrawerVisible = ref(false); const importDrawerVisible = ref(false);
const offspringIds = ref<string[]>([]); const offspringIds = ref<string[]>([]);
const protocol = ref('HTTP'); const protocol = ref('HTTP');
@ -72,9 +74,10 @@
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>(); const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const managementRef = ref<InstanceType<typeof management>>(); const managementRef = ref<InstanceType<typeof management>>();
function handleModuleInit(tree, _protocol: string) { function handleModuleInit(tree, _protocol: string, pathMap: Record<string, any>) {
folderTree.value = tree; folderTree.value = tree;
protocol.value = _protocol; protocol.value = _protocol;
folderTreePathMap.value = pathMap;
} }
function newApi() { function newApi() {
@ -102,9 +105,15 @@
moduleTreeRef.value?.refresh(); moduleTreeRef.value?.refresh();
} }
/** 向子孙组件提供方法 */ function handleImportDone() {
refreshModuleTree();
managementRef.value?.refreshApiTable();
}
/** 向子孙组件提供方法和值 */
provide('setActiveApi', setActiveApi); provide('setActiveApi', setActiveApi);
provide('refreshModuleTree', refreshModuleTree); provide('refreshModuleTree', refreshModuleTree);
provide('folderTreePathMap', folderTreePathMap.value);
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -3,6 +3,15 @@ export default {
'apiTestManagement.importApi': 'Import api', 'apiTestManagement.importApi': 'Import api',
'apiTestManagement.fileImport': 'Import file', 'apiTestManagement.fileImport': 'Import file',
'apiTestManagement.timeImport': 'Scheduled import', 'apiTestManagement.timeImport': 'Scheduled import',
'apiTestManagement.timeTask': 'Timed tasks',
'apiTestManagement.name': 'Task name',
'apiTestManagement.taskRunRule': 'Run rules',
'apiTestManagement.taskNextRunTime': 'Next execution time',
'apiTestManagement.taskOperator': 'Operator',
'apiTestManagement.taskOperationTime': 'Operating time',
'apiTestManagement.createTaskSuccess': 'Create scheduled import task successfully',
'apiTestManagement.enableTaskSuccess': 'Start scheduled import task successfully',
'apiTestManagement.disableTaskSuccess': 'Closing the scheduled import task successfully',
'apiTestManagement.addSubModule': 'Add submodule', 'apiTestManagement.addSubModule': 'Add submodule',
'apiTestManagement.allApi': 'All api', 'apiTestManagement.allApi': 'All api',
'apiTestManagement.searchTip': 'Please enter module/api name', 'apiTestManagement.searchTip': 'Please enter module/api name',

View File

@ -3,6 +3,15 @@ export default {
'apiTestManagement.importApi': '导入接口', 'apiTestManagement.importApi': '导入接口',
'apiTestManagement.fileImport': '文件导入', 'apiTestManagement.fileImport': '文件导入',
'apiTestManagement.timeImport': '定时导入', 'apiTestManagement.timeImport': '定时导入',
'apiTestManagement.timeTask': '定时任务',
'apiTestManagement.name': '名称',
'apiTestManagement.taskRunRule': '运行规则',
'apiTestManagement.taskNextRunTime': '下次执行时间',
'apiTestManagement.taskOperator': '操作人',
'apiTestManagement.taskOperationTime': '操作时间',
'apiTestManagement.createTaskSuccess': '创建定时导入任务成功',
'apiTestManagement.enableTaskSuccess': '开启定时导入任务成功',
'apiTestManagement.disableTaskSuccess': '关闭定时导入任务成功',
'apiTestManagement.addSubModule': '添加子模块', 'apiTestManagement.addSubModule': '添加子模块',
'apiTestManagement.allApi': '全部接口', 'apiTestManagement.allApi': '全部接口',
'apiTestManagement.searchTip': '请输入模块/接口名称', 'apiTestManagement.searchTip': '请输入模块/接口名称',

View File

@ -106,7 +106,7 @@
<BugCaseTab <BugCaseTab
v-else-if="activeTab === 'case'" v-else-if="activeTab === 'case'"
:bug-id="detailInfo.id" :bug-id="detailInfo.id"
@updateCaseSuccess="updateSuccess" @update-case-success="updateSuccess"
/> />
<CommentTab v-else-if="activeTab === 'comment'" ref="commentRef" :bug-id="detailInfo.id" /> <CommentTab v-else-if="activeTab === 'comment'" ref="commentRef" :bug-id="detailInfo.id" />
@ -170,7 +170,7 @@
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type'; import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { CommentInput } from '@/components/business/ms-comment'; import CommentInput from '@/components/business/ms-comment/input.vue';
import { CommentParams } from '@/components/business/ms-comment/types'; import { CommentParams } from '@/components/business/ms-comment/types';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue'; import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import BugCaseTab from './bugCaseTab.vue'; import BugCaseTab from './bugCaseTab.vue';

View File

@ -3,7 +3,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MsComment from '@/components/business/ms-comment'; import MsComment from '@/components/business/ms-comment/comment';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types'; import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import { createOrUpdateComment, deleteComment, getCommentList } from '@/api/modules/bug-management/index'; import { createOrUpdateComment, deleteComment, getCommentList } from '@/api/modules/bug-management/index';

View File

@ -68,7 +68,7 @@
import MSAvatar from '@/components/pure/ms-avatar/index.vue'; import MSAvatar from '@/components/pure/ms-avatar/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue'; import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsComment from '@/components/business/ms-comment'; import MsComment from '@/components/business/ms-comment/comment';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types'; import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import { import {