feat(接口管理): 接口定义-导入&前后置数据源切换
This commit is contained in:
parent
0c8103011b
commit
4332f78df3
|
@ -142,7 +142,7 @@
|
|||
"stylelint-less": "^1.0.8",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.4.2",
|
||||
"unplugin-auto-import": "^0.16.7",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^3.2.7",
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
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) {
|
||||
|
@ -17,3 +31,18 @@ export function getPluginOptions(data: GetPluginOptionsParams) {
|
|||
export function getPluginScript(pluginId: string) {
|
||||
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 });
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
GetApiDebugDetailUrl,
|
||||
GetDebugModuleCountUrl,
|
||||
GetDebugModulesUrl,
|
||||
LocalExecuteApiDebugUrl,
|
||||
MoveDebugModuleUrl,
|
||||
TestMockUrl,
|
||||
TransferFileUrl,
|
||||
|
@ -69,11 +68,6 @@ export function executeDebug(data: ExecuteRequestParams) {
|
|||
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) {
|
||||
return MSR.post({ url: AddApiDebugUrl, data });
|
||||
|
|
|
@ -1,28 +1,43 @@
|
|||
import MSR from '@/api/http/index';
|
||||
import {
|
||||
AddDefinitionScheduleUrl,
|
||||
AddDefinitionUrl,
|
||||
AddModuleUrl,
|
||||
BatchDeleteDefinitionUrl,
|
||||
BatchMoveDefinitionUrl,
|
||||
BatchUpdateDefinitionUrl,
|
||||
CheckDefinitionScheduleUrl,
|
||||
DebugDefinitionUrl,
|
||||
DefinitionMockPageUrl,
|
||||
DefinitionPageUrl,
|
||||
DeleteDefinitionScheduleUrl,
|
||||
DeleteDefinitionUrl,
|
||||
DeleteMockUrl,
|
||||
DeleteModuleUrl,
|
||||
GetDefinitionDetailUrl,
|
||||
GetDefinitionScheduleUrl,
|
||||
GetEnvModuleUrl,
|
||||
GetModuleCountUrl,
|
||||
GetModuleOnlyTreeUrl,
|
||||
GetModuleTreeUrl,
|
||||
ImportDefinitionUrl,
|
||||
MoveModuleUrl,
|
||||
SortDefinitionUrl,
|
||||
SwitchDefinitionScheduleUrl,
|
||||
TransferFileModuleOptionUrl,
|
||||
TransferFileUrl,
|
||||
UpdateDefinitionScheduleUrl,
|
||||
UpdateDefinitionUrl,
|
||||
UpdateMockStatusUrl,
|
||||
UpdateModuleUrl,
|
||||
UploadTempFileUrl,
|
||||
} from '@/api/requrls/api-test/management';
|
||||
|
||||
import { ExecuteRequestParams } from '@/models/apiTest/common';
|
||||
import {
|
||||
ApiDefinitionBatchDeleteParams,
|
||||
ApiDefinitionBatchMoveParams,
|
||||
ApiDefinitionBatchUpdateParams,
|
||||
ApiDefinitionCreateParams,
|
||||
ApiDefinitionDetail,
|
||||
ApiDefinitionGetEnvModuleParams,
|
||||
|
@ -32,10 +47,21 @@ import {
|
|||
ApiDefinitionPageParams,
|
||||
ApiDefinitionUpdateModuleParams,
|
||||
ApiDefinitionUpdateParams,
|
||||
CheckScheduleParams,
|
||||
CreateImportApiDefinitionScheduleParams,
|
||||
EnvModule,
|
||||
ImportApiDefinitionParams,
|
||||
mockParams,
|
||||
UpdateScheduleParams,
|
||||
} 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) {
|
||||
|
@ -94,7 +120,7 @@ export function updateDefinition(data: ApiDefinitionUpdateParams) {
|
|||
|
||||
// 获取接口定义详情
|
||||
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) {
|
||||
return MSR.get({ url: BatchDeleteDefinitionUrl, params: id });
|
||||
export function batchDeleteDefinition(data: ApiDefinitionBatchDeleteParams) {
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
export const GetProtocolListUrl = '/api/test/protocol'; // 获取协议列表
|
||||
export const GetPluginOptionsUrl = '/api/test/plugin/form/option'; // 获取插件表单选项
|
||||
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'; // 获取接口测试环境详情
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export const ExecuteApiDebugUrl = '/api/debug/debug'; // 执行调试
|
||||
export const LocalExecuteApiDebugUrl = '/api/debug'; // 本地执行调试
|
||||
export const AddApiDebugUrl = '/api/debug/add'; // 新增调试
|
||||
export const UpdateApiDebugUrl = '/api/debug/update'; // 更新调试
|
||||
export const GetApiDebugDetailUrl = '/api/debug/get'; // 获取接口调试详情
|
||||
|
|
|
@ -13,8 +13,20 @@ export const GetDefinitionDetailUrl = '/api/definition/get-detail'; // 获取接
|
|||
export const TransferFileUrl = '/api/definition/transfer'; // 文件转存
|
||||
export const TransferFileModuleOptionUrl = '/api/definition/transfer/options'; // 文件转存目录
|
||||
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 UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新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'; // 接口定义-调试
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -160,7 +160,7 @@
|
|||
|
||||
import { APIKEY } from '@/models/user';
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const { copy, isSupported } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
|
@ -226,8 +226,12 @@
|
|||
];
|
||||
|
||||
async function handleCopy(val: string) {
|
||||
await copy(val);
|
||||
Message.success(t('ms.personal.copySuccess'));
|
||||
if (isSupported) {
|
||||
await copy(val);
|
||||
Message.success(t('ms.personal.copySuccess'));
|
||||
} else {
|
||||
Message.warning(t('common.copyNotSupport'));
|
||||
}
|
||||
}
|
||||
|
||||
function desensitization(item: APIKEYItem) {
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
<slot name="title" v-bind="_props"></slot>
|
||||
</a-tooltip>
|
||||
</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">
|
||||
<div
|
||||
v-if="_props.hideMoreAction !== true"
|
||||
|
@ -298,6 +301,7 @@
|
|||
dropNode: MsTreeNodeData; // 放入的节点
|
||||
dropPosition: number; // 放入的位置,-1 为放入节点前,1 为放入节点后,0 为放入节点内
|
||||
}) {
|
||||
console.log('dropNode', dropNode);
|
||||
loop(originalTreeData.value, dragNode.key, (item, index, arr) => {
|
||||
arr.splice(index, 1);
|
||||
});
|
||||
|
|
|
@ -120,6 +120,9 @@
|
|||
const { arrivedState } = useScroll(tabNav);
|
||||
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
||||
|
||||
/**
|
||||
* 滚动tab
|
||||
*/
|
||||
const scrollTabs = (direction: 'left' | 'right') => {
|
||||
if (tabNav.value) {
|
||||
const tabNavWidth = tabNav.value?.clientWidth || 0;
|
||||
|
@ -139,6 +142,9 @@
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 滚动到当前激活的tab
|
||||
*/
|
||||
const scrollToActiveTab = () => {
|
||||
const activeTabDom = tabNav.value?.querySelector('.ms-editable-tab.active');
|
||||
if (activeTabDom) {
|
||||
|
@ -169,6 +175,9 @@
|
|||
return props.moreActionList ? [...dl, ...props.moreActionList] : dl;
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听激活的tab变化,滚动到激活的tab
|
||||
*/
|
||||
watch(
|
||||
() => props.activeTab,
|
||||
() => {
|
||||
|
@ -192,14 +201,21 @@
|
|||
emit('add');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭一个tab
|
||||
*/
|
||||
function closeOneTab(item: TabItem) {
|
||||
const index = innerTabs.value.findIndex((e) => e.id === item.id);
|
||||
innerTabs.value.splice(index, 1);
|
||||
if (innerActiveTab.value?.id === item.id && innerTabs.value[0]) {
|
||||
[innerActiveTab.value] = innerTabs.value;
|
||||
emit('change', innerTabs.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭tab前处理
|
||||
*/
|
||||
function close(item: TabItem) {
|
||||
if (item.unSaved) {
|
||||
openModal({
|
||||
|
@ -219,18 +235,24 @@
|
|||
}
|
||||
|
||||
function handleTabClick(item: TabItem) {
|
||||
innerActiveTab.value = item;
|
||||
nextTick(() => {
|
||||
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
emit('change', item);
|
||||
if (innerActiveTab.value?.id !== item.id) {
|
||||
innerActiveTab.value = item;
|
||||
nextTick(() => {
|
||||
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
emit('change', item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更多操作
|
||||
*/
|
||||
function executeAction(event: ActionsItem) {
|
||||
switch (event.eventTag) {
|
||||
case 'closeAll':
|
||||
innerTabs.value = innerTabs.value.filter((item) => item.closable === false);
|
||||
[innerActiveTab.value] = innerTabs.value;
|
||||
emit('change', innerActiveTab.value);
|
||||
break;
|
||||
case 'closeOther':
|
||||
innerTabs.value = innerTabs.value.filter(
|
||||
|
@ -243,6 +265,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更多操作选择
|
||||
*/
|
||||
function handleMoreActionSelect(event: ActionsItem) {
|
||||
if (
|
||||
(event.eventTag === 'closeAll' && innerTabs.value.some((item) => item.unSaved)) ||
|
||||
|
|
|
@ -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 showSetting为true时必传
|
||||
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>
|
|
@ -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>
|
|
@ -69,12 +69,14 @@
|
|||
const { t } = useI18n();
|
||||
|
||||
const innerModelValue = ref(props.modelValue);
|
||||
const innerInputValue = ref(props.inputValue);
|
||||
const innerInputValue = defineModel<string>('inputValue', {
|
||||
default: '',
|
||||
});
|
||||
const tagsLength = ref(0); // 记录每次回车或失去焦点前的tags长度,以判断是否有新的tag被添加,新标签添加时需要判断是否重复的标签
|
||||
|
||||
const isError = computed(
|
||||
() =>
|
||||
(innerInputValue.value || '').length > props.maxLength ||
|
||||
innerInputValue.value.length > props.maxLength ||
|
||||
innerModelValue.value.some((item) => item.toString().length > props.maxLength)
|
||||
);
|
||||
watch(
|
||||
|
@ -97,20 +99,6 @@
|
|||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.inputValue,
|
||||
(val) => {
|
||||
innerInputValue.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => innerInputValue.value,
|
||||
(val) => {
|
||||
emit('update:inputValue', val);
|
||||
}
|
||||
);
|
||||
|
||||
function validateTagsCountEnter() {
|
||||
if (innerModelValue.value.length > 10) {
|
||||
innerModelValue.value.pop();
|
||||
|
@ -160,7 +148,8 @@
|
|||
if (
|
||||
validateTagsCountEnter() &&
|
||||
validateUniqueValue() &&
|
||||
(innerInputValue.value || '').trim().length <= props.maxLength
|
||||
innerInputValue.value &&
|
||||
innerInputValue.value.trim().length <= props.maxLength
|
||||
) {
|
||||
innerInputValue.value = '';
|
||||
tagsLength.value += 1;
|
||||
|
|
|
@ -220,9 +220,9 @@
|
|||
}
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
function copyVersion() {
|
||||
async function copyVersion() {
|
||||
if (isSupported) {
|
||||
copy(appStore.version);
|
||||
await copy(appStore.version);
|
||||
Message.success(t('common.copySuccess'));
|
||||
} else {
|
||||
Message.warning(t('common.copyNotSupport'));
|
||||
|
|
|
@ -56,6 +56,7 @@ export enum ResponseComposition {
|
|||
}
|
||||
// 接口响应体格式
|
||||
export enum ResponseBodyFormat {
|
||||
NONE = 'NONE',
|
||||
JSON = 'JSON',
|
||||
XML = 'XML',
|
||||
RAW = 'RAW',
|
||||
|
@ -70,7 +71,17 @@ export enum RequestDefinitionStatus {
|
|||
}
|
||||
// 接口导入支持格式
|
||||
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 {
|
||||
|
|
|
@ -254,14 +254,13 @@ export interface ExecuteConditionProcessorCommon {
|
|||
export type ScriptProcessor = ScriptCommonConfig & ExecuteConditionProcessorCommon;
|
||||
// 执行请求-前后置条件-SQL脚本处理器
|
||||
export interface SQLProcessor extends ExecuteConditionProcessorCommon {
|
||||
description: string; // 描述
|
||||
name: string; // 描述
|
||||
dataSourceId: string; // 数据源ID
|
||||
environmentId: string; // 环境ID
|
||||
dataSourceName: string; // 数据源名称
|
||||
queryTimeout: number; // 超时时间
|
||||
resultVariable: string; // 按结果存储时的结果变量
|
||||
script: string; // 脚本内容
|
||||
variableNames: string; // 按列存储时的变量名集合,多个列可以使用逗号分隔
|
||||
variables: EnableKeyValueParam[]; // 变量列表
|
||||
extractParams: KeyValueParam[]; // 提取参数列表
|
||||
}
|
||||
// 执行请求-前后置条件-等待时间处理器
|
||||
|
|
|
@ -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';
|
||||
|
||||
// 定义-自定义字段
|
||||
|
@ -13,7 +15,7 @@ export interface ApiDefinitionCreateParams extends ExecuteRequestParams {
|
|||
tags: string[];
|
||||
response: ResponseDefinition;
|
||||
description: string;
|
||||
status: string;
|
||||
status: RequestDefinitionStatus;
|
||||
customFields: ApiDefinitionCustomField[];
|
||||
moduleId: 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;
|
||||
deleteFileIds: string[];
|
||||
unLinkFileIds: string[];
|
||||
deleteFileIds?: string[];
|
||||
unLinkFileIds?: string[];
|
||||
}
|
||||
|
||||
// 定义-自定义字段详情
|
||||
|
@ -164,3 +166,67 @@ export interface mockParams {
|
|||
id: 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;
|
||||
}
|
||||
|
|
|
@ -135,7 +135,6 @@ export interface BatchReviewCaseParams extends BatchApiParams {
|
|||
status: ReviewResult; // 评审结果
|
||||
content: string; // 评论内容
|
||||
notifier: string; // 评论@的人的Id, 多个以';'隔开
|
||||
moduleIds: (string | number)[];
|
||||
}
|
||||
// 评审详情-批量修改评审人
|
||||
export interface BatchChangeReviewerParams extends BatchApiParams {
|
||||
|
@ -143,13 +142,11 @@ export interface BatchChangeReviewerParams extends BatchApiParams {
|
|||
userId: string; // 用户id, 用来判断是否只看我的
|
||||
reviewerId: string[]; // 评审人员id
|
||||
append: boolean; // 是否追加
|
||||
moduleIds: (string | number)[];
|
||||
}
|
||||
// 评审详情-批量取消关联用例
|
||||
export interface BatchCancelReviewCaseParams extends BatchApiParams {
|
||||
reviewId: string; // 评审id
|
||||
userId: string; // 用户id, 用来判断是否只看我的
|
||||
moduleIds: (string | number)[];
|
||||
}
|
||||
export interface ReviewDetailReviewersItem {
|
||||
avatar: string;
|
||||
|
|
|
@ -44,6 +44,8 @@ export interface BatchApiParams {
|
|||
selectAll: boolean; // 是否跨页全选,即选择当前筛选条件下的全部表格数据
|
||||
condition: Record<string, any>; // 当前表格查询的筛选条件
|
||||
currentSelectCount?: number; // 当前已选择的数量
|
||||
projectId?: string; // 项目 ID
|
||||
moduleIds?: (string | number)[]; // 模块 ID 集合
|
||||
}
|
||||
|
||||
// 移动模块树
|
||||
|
|
|
@ -155,3 +155,16 @@ export interface HttpForm {
|
|||
condition: '';
|
||||
};
|
||||
}
|
||||
// 环境列表项
|
||||
export interface EnvironmentItem {
|
||||
id: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
createUser: string;
|
||||
updateUser: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
mock: boolean;
|
||||
description: string;
|
||||
pos: number;
|
||||
}
|
||||
|
|
|
@ -217,14 +217,15 @@
|
|||
<div class="mb-[16px]">
|
||||
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
|
||||
<a-input
|
||||
v-model:model-value="condition.description"
|
||||
v-model:model-value="condition.name"
|
||||
:placeholder="t('apiTestDebug.commonPlaceholder')"
|
||||
:max-length="255"
|
||||
@input="() => emit('change')"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
|
||||
<div class="text-[var(--color-text-2)]">
|
||||
{{ condition.scriptName || '-' }}
|
||||
{{ condition.dataSourceName || '-' }}
|
||||
</div>
|
||||
<a-divider margin="8px" direction="vertical" />
|
||||
<MsButton type="text" class="font-medium" @click="quoteSqlSourceDrawerVisible = true">
|
||||
|
@ -240,48 +241,52 @@
|
|||
:language="LanguageEnum.SQL"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
read-only
|
||||
@change="() => emit('change')"
|
||||
>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
<div class="mb-[16px]">
|
||||
<div class="mb-[8px] flex items-center text-[var(--color-text-1)]">
|
||||
{{ t('apiTestDebug.storageType') }}
|
||||
<a-tooltip position="right">
|
||||
{{ t('apiTestDebug.storageByCol') }}
|
||||
<a-tooltip position="right" :content="t('apiTestDebug.storageColTip')">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('apiTestDebug.storageTypeTip1') }}</div>
|
||||
<div>{{ t('apiTestDebug.storageTypeTip2') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-[16px]">
|
||||
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByCol') }}</div>
|
||||
<a-input
|
||||
v-model:model-value="condition.variableNames"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiTestDebug.storageByColPlaceholder', { a: '{id_1}', b: '{username_1}' })"
|
||||
@input="() => emit('change')"
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
v-model:params="condition.variables"
|
||||
v-model:params="condition.extractParams"
|
||||
:columns="sqlSourceColumns"
|
||||
:selectable="false"
|
||||
:default-param-item="defaultKeyValueParamItem"
|
||||
@change="handleSqlSourceParamTableChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-[16px]">
|
||||
<div class="mt-[16px]">
|
||||
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div>
|
||||
<a-input
|
||||
v-model:model-value="condition.resultVariable"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })"
|
||||
@input="() => emit('change')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -431,6 +436,7 @@
|
|||
import type { ProtocolItem } from '@/models/apiTest/common';
|
||||
import { ExecuteConditionProcessor, JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common';
|
||||
import { ParamsRequestType } from '@/models/projectManagement/commonScript';
|
||||
import { DataSourceItem, EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import {
|
||||
RequestConditionProcessor,
|
||||
RequestExtractEnvType,
|
||||
|
@ -441,6 +447,8 @@
|
|||
ResponseBodyXPathAssertionFormat,
|
||||
} from '@/enums/apiEnum';
|
||||
|
||||
import { defaultKeyValueParamItem } from '@/views/api-test/components/config';
|
||||
|
||||
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
|
||||
const appStore = useAppStore();
|
||||
const props = withDefaults(
|
||||
|
@ -468,7 +476,31 @@
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
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 scriptNameInputRef = ref<InputInstance>();
|
||||
|
@ -490,9 +522,9 @@ if (!result){
|
|||
}`);
|
||||
const { copy, isSupported } = useClipboard();
|
||||
|
||||
function copyScriptEx() {
|
||||
async function copyScriptEx() {
|
||||
if (isSupported) {
|
||||
copy(scriptEx.value);
|
||||
await copy(scriptEx.value);
|
||||
Message.success(t('apiTestDebug.scriptExCopySuccess'));
|
||||
} else {
|
||||
Message.warning(t('apiTestDebug.copyNotSupport'));
|
||||
|
@ -598,14 +630,14 @@ if (!result){
|
|||
},
|
||||
];
|
||||
const quoteSqlSourceDrawerVisible = ref(false);
|
||||
function handleQuoteSqlSourceApply(sqlSource: Record<string, any>) {
|
||||
condition.value.script = sqlSource.script;
|
||||
function handleQuoteSqlSourceApply(sqlSource: DataSourceItem) {
|
||||
condition.value.dataSourceName = sqlSource.dataSource;
|
||||
condition.value.dataSourceId = sqlSource.id;
|
||||
emit('change');
|
||||
}
|
||||
|
||||
function handleSqlSourceParamTableChange(resultArr: any[], isInit?: boolean) {
|
||||
condition.value.variables = [...resultArr];
|
||||
condition.value.extractParams = [...resultArr];
|
||||
if (!isInit) {
|
||||
emit('change');
|
||||
}
|
||||
|
|
|
@ -169,15 +169,13 @@
|
|||
associateScenarioResult: false,
|
||||
ignoreProtocols: [],
|
||||
beforeStepScript: true,
|
||||
description: '',
|
||||
name: '',
|
||||
enable: true,
|
||||
dataSourceId: '',
|
||||
environmentId: '',
|
||||
queryTimeout: 0,
|
||||
resultVariable: '',
|
||||
script: '',
|
||||
variableNames: '',
|
||||
variables: [],
|
||||
extractParams: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
EnableKeyValueParam,
|
||||
ExecuteRequestCommonParam,
|
||||
ExecuteRequestFormBodyFormValue,
|
||||
KeyValueParam,
|
||||
ResponseDefinition,
|
||||
} from '@/models/apiTest/common';
|
||||
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 的响应状态码集合
|
||||
export const statusCodes = [200, 201, 202, 203, 204, 205, 400, 401, 402, 403, 404, 405, 500, 501, 502, 503, 504, 505];
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
<template>
|
||||
<MsBaseTable
|
||||
v-bind="propsRes"
|
||||
:hoverable="false"
|
||||
no-disable
|
||||
is-simple-setting
|
||||
:span-method="props.spanMethod"
|
||||
v-on="propsEvent"
|
||||
>
|
||||
<MsFormTable v-bind="props" :data="paramsData">
|
||||
<!-- 展开行-->
|
||||
<template #expand-icon="{ record }">
|
||||
<div class="flex flex-row items-center gap-[2px] text-[var(--color-text-4)]">
|
||||
|
@ -404,7 +397,7 @@
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
</MsBaseTable>
|
||||
</MsFormTable>
|
||||
<a-modal
|
||||
v-model:visible="showQuickInputParam"
|
||||
:title="t('ms.paramsInput.value')"
|
||||
|
@ -463,9 +456,7 @@
|
|||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.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 MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
|
||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
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 { useI18n } from '@/hooks/useI18n';
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
||||
import { ProjectOptionItem } from '@/models/projectManagement/environmental';
|
||||
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
|
||||
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
import { filterKeyValParams } from './utils';
|
||||
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 MsParamsInput = defineAsyncComponent(() => import('@/components/business/ms-params-input/index.vue'));
|
||||
|
||||
export type ParamTableColumn = MsTableColumnData & {
|
||||
export interface ParamTableColumn extends FormTableColumn {
|
||||
isNormal?: boolean; // 用于 value 列区分是普通输入框还是 MsParamsInput
|
||||
hasRequired?: boolean; // 用于 type 列区分是否有 required 星号
|
||||
typeOptions?: { label: string; value: string }[]; // 用于 type 列选择器选项
|
||||
|
@ -499,7 +489,7 @@
|
|||
moreAction?: ActionsItem[]; // 用于 operation 列更多操作按钮配置
|
||||
format?: RequestBodyFormat; // 用于 operation 列区分是否有请求体格式选择器
|
||||
addLineDisabled?: boolean; // 用于 是否禁用添加新行
|
||||
};
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -565,75 +555,22 @@
|
|||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
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,
|
||||
isSimpleSetting: props.isSimpleSetting,
|
||||
showPagination: false,
|
||||
});
|
||||
const paramsData = ref<any[]>(props.params);
|
||||
|
||||
function emitChange(from: string, isInit?: boolean) {
|
||||
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));
|
||||
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);
|
||||
const paramsLength = computed(() => paramsData.value.length);
|
||||
|
||||
function deleteParam(record: Record<string, any>, rowIndex: number) {
|
||||
if (props.isTreeTable) {
|
||||
emit('treeDelete', record);
|
||||
return;
|
||||
}
|
||||
propsRes.value.data.splice(rowIndex, 1);
|
||||
paramsData.value.splice(rowIndex, 1);
|
||||
emitChange('deleteParam');
|
||||
}
|
||||
|
||||
|
@ -644,17 +581,14 @@
|
|||
const handleMustIncludeChange = (val: boolean) => {
|
||||
mustIncludeAllChecked.value = val;
|
||||
mustIncludeIndeterminate.value = false;
|
||||
const { data } = propsRes.value;
|
||||
data.forEach((e: any) => {
|
||||
paramsData.value.forEach((e: any) => {
|
||||
e.mustInclude = val;
|
||||
});
|
||||
propsRes.value.data = data;
|
||||
emitChange('handleMustIncludeChange');
|
||||
};
|
||||
const handleMustContainColChange = (notEmit?: boolean) => {
|
||||
const { data } = propsRes.value;
|
||||
const checkedList = data.filter((e: any) => e.mustInclude).map((e: any) => e.id);
|
||||
if (checkedList.length === data.length) {
|
||||
const checkedList = paramsData.value.filter((e: any) => e.mustInclude).map((e: any) => e.id);
|
||||
if (checkedList.length === paramsData.value.length) {
|
||||
mustIncludeAllChecked.value = true;
|
||||
mustIncludeIndeterminate.value = false;
|
||||
} else if (checkedList.length === 0) {
|
||||
|
@ -674,17 +608,14 @@
|
|||
const handleTypeCheckingChange = (val: boolean) => {
|
||||
typeCheckingAllChecked.value = val;
|
||||
typeCheckingIndeterminate.value = false;
|
||||
const { data } = propsRes.value;
|
||||
data.forEach((e: any) => {
|
||||
paramsData.value.forEach((e: any) => {
|
||||
e.typeChecking = val;
|
||||
});
|
||||
propsRes.value.data = data;
|
||||
emitChange('handleTypeCheckingChange');
|
||||
};
|
||||
const handleTypeCheckingColChange = (notEmit?: boolean) => {
|
||||
const { data } = propsRes.value;
|
||||
const checkedList = data.filter((e: any) => e.typeChecking).map((e: any) => e.id);
|
||||
if (checkedList.length === data.length) {
|
||||
const checkedList = paramsData.value.filter((e: any) => e.typeChecking).map((e: any) => e.id);
|
||||
if (checkedList.length === paramsData.value.length) {
|
||||
typeCheckingAllChecked.value = true;
|
||||
typeCheckingIndeterminate.value = false;
|
||||
} else if (checkedList.length === 0) {
|
||||
|
@ -766,10 +697,10 @@
|
|||
if (addLineDisabled) {
|
||||
return;
|
||||
}
|
||||
if (rowIndex === propsRes.value.data.length - 1) {
|
||||
if (rowIndex === paramsData.value.length - 1) {
|
||||
// 最后一行的更改才会触发添加新一行
|
||||
const id = new Date().getTime().toString();
|
||||
propsRes.value.data.push({
|
||||
paramsData.value.push({
|
||||
id,
|
||||
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
||||
enable: true, // 是否勾选
|
||||
|
@ -785,7 +716,14 @@
|
|||
(arr) => {
|
||||
if (arr.length > 0) {
|
||||
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) {
|
||||
// 后台存储无id,渲染时需要手动添加一次
|
||||
hasNoIdItem = true;
|
||||
|
@ -801,7 +739,7 @@
|
|||
}
|
||||
} else {
|
||||
const id = new Date().getTime().toString();
|
||||
propsRes.value.data = [
|
||||
paramsData.value = [
|
||||
{
|
||||
id, // 默认给时间戳 id,若 props.defaultParamItem 有 id,则覆盖
|
||||
...props.defaultParamItem,
|
||||
|
@ -880,7 +818,7 @@
|
|||
function applyQuickInputParam() {
|
||||
activeQuickInputRecord.value.value = quickInputParamValue.value;
|
||||
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();
|
||||
emitChange('applyQuickInputParam');
|
||||
}
|
||||
|
@ -902,7 +840,7 @@
|
|||
function applyQuickInputDesc() {
|
||||
activeQuickInputRecord.value.description = quickInputDescValue.value;
|
||||
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();
|
||||
emitChange('applyQuickInputDesc');
|
||||
}
|
||||
|
@ -959,61 +897,9 @@
|
|||
defineExpose({
|
||||
addTableLine,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-type-trigger-content {
|
||||
@apply bg-white;
|
||||
|
||||
|
@ -1043,7 +929,4 @@
|
|||
line-height: 16px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
:deep(.arco-table-expand-btn) {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,8 +13,9 @@
|
|||
:placeholder="t('project.projectVersion.searchPlaceholder')"
|
||||
class="w-[230px]"
|
||||
allow-clear
|
||||
@search="searchSource"
|
||||
@press-enter="searchSource"
|
||||
@search="searchDataSource"
|
||||
@press-enter="searchDataSource"
|
||||
@clear="searchDataSource"
|
||||
/>
|
||||
</div>
|
||||
<MsBaseTable v-bind="propsRes" v-model:selected-key="selectedKey" v-on="propsEvent">
|
||||
|
@ -29,6 +30,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
|
@ -37,6 +39,8 @@
|
|||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
selectedKey?: string;
|
||||
|
@ -48,6 +52,8 @@
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
/** 接收祖先组件提供的属性 */
|
||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
const innerVisible = useVModel(props, 'visible', emit);
|
||||
const keyword = ref('');
|
||||
const selectedKey = ref(props.selectedKey || '');
|
||||
|
@ -55,7 +61,7 @@
|
|||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'apiTestDebug.sqlSourceName',
|
||||
dataIndex: 'name',
|
||||
dataIndex: 'dataSource',
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
|
@ -71,7 +77,7 @@
|
|||
},
|
||||
{
|
||||
title: 'apiTestDebug.maxConnection',
|
||||
dataIndex: 'maxConnection',
|
||||
dataIndex: 'poolMax',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
|
@ -82,47 +88,8 @@
|
|||
width: 120,
|
||||
},
|
||||
];
|
||||
async function loadSource() {
|
||||
return Promise.resolve({
|
||||
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, {
|
||||
|
||||
const { propsRes, propsEvent } = useTable(undefined, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
heightUsed: 300,
|
||||
|
@ -130,14 +97,28 @@
|
|||
showSelectorAll: false,
|
||||
selectorType: 'radio',
|
||||
firstColumnWidth: 44,
|
||||
showPagination: false,
|
||||
});
|
||||
function searchSource() {
|
||||
setLoadListParams({
|
||||
keyword: keyword.value,
|
||||
});
|
||||
loadList();
|
||||
|
||||
watch(
|
||||
() => currentEnvConfig?.value,
|
||||
(config) => {
|
||||
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() {
|
||||
innerVisible.value = false;
|
||||
|
|
|
@ -339,6 +339,7 @@
|
|||
|
||||
import {
|
||||
ExecuteApiRequestFullParams,
|
||||
ExecuteConditionConfig,
|
||||
ExecuteRequestParams,
|
||||
PluginConfig,
|
||||
RequestTaskResult,
|
||||
|
@ -348,6 +349,7 @@
|
|||
RequestAuthType,
|
||||
RequestBodyFormat,
|
||||
RequestComposition,
|
||||
RequestConditionProcessor,
|
||||
RequestMethods,
|
||||
RequestParamsType,
|
||||
} from '@/enums/apiEnum';
|
||||
|
@ -356,6 +358,7 @@
|
|||
import {
|
||||
defaultBodyParamsItem,
|
||||
defaultHeaderParamsItem,
|
||||
defaultKeyValueParamItem,
|
||||
defaultRequestParamsItem,
|
||||
} from '@/views/api-test/components/config';
|
||||
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 执行类型,执行时传入
|
||||
|
@ -930,12 +947,11 @@
|
|||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
// TODO:暂时不做断言
|
||||
enableGlobal: false,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: requestVModel.value.children[0].postProcessorConfig,
|
||||
preProcessorConfig: requestVModel.value.children[0].preProcessorConfig,
|
||||
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
|
||||
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -193,6 +193,15 @@
|
|||
});
|
||||
const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
|
||||
|
||||
watch(
|
||||
() => responseTabs.value,
|
||||
(arr) => {
|
||||
if (arr[0]) {
|
||||
[activeResponse.value] = arr;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
|
||||
responseTabs.value.push({
|
||||
...cloneDeep(defaultResponseItem),
|
||||
|
|
|
@ -117,8 +117,8 @@
|
|||
</div>
|
||||
<a-spin :loading="props.loading" class="h-[calc(100%-35px)] w-full px-[18px] pb-[18px]">
|
||||
<edit
|
||||
v-if="props.isEdit && activeResponseType === 'content' && props.responseDefinition"
|
||||
:response-definition="props.responseDefinition"
|
||||
v-if="props.isEdit && activeResponseType === 'content' && validResponseDefinition"
|
||||
:response-definition="validResponseDefinition"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
@change="handleResponseChange"
|
||||
/>
|
||||
|
@ -144,7 +144,7 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { RequestTaskResult } from '@/models/apiTest/common';
|
||||
import { ResponseComposition } from '@/enums/apiEnum';
|
||||
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -215,6 +215,45 @@
|
|||
}
|
||||
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() {
|
||||
emit('change');
|
||||
|
@ -229,7 +268,7 @@
|
|||
watch(
|
||||
() => props.requestTaskResult,
|
||||
(task) => {
|
||||
if (task) {
|
||||
if (task?.requestResults[0]?.responseResult?.responseCode) {
|
||||
setActiveResponse('result');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,9 +131,9 @@
|
|||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
|
||||
function copyScript() {
|
||||
async function copyScript() {
|
||||
if (isSupported) {
|
||||
copy(props.requestResult?.responseResult.body || '');
|
||||
await copy(props.requestResult?.responseResult.body || '');
|
||||
Message.success(t('common.copySuccess'));
|
||||
} else {
|
||||
Message.warning(t('apiTestDebug.copyNotSupport'));
|
||||
|
|
|
@ -3,8 +3,6 @@ import { cloneDeep, isEqual } from 'lodash-es';
|
|||
import { ExecuteBody } from '@/models/apiTest/common';
|
||||
import { RequestParamsType } from '@/enums/apiEnum';
|
||||
|
||||
export default {};
|
||||
|
||||
export interface ParseResult {
|
||||
uploadFileIds: string[];
|
||||
linkFileIds: string[];
|
||||
|
@ -108,7 +106,7 @@ export function parseRequestBodyFiles(
|
|||
* @param params 原始参数数组
|
||||
* @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 defaultParam = cloneDeep(defaultParamItem);
|
||||
if (!lastData || !defaultParam) {
|
||||
|
@ -123,7 +121,7 @@ export function filterKeyValParams(params: Record<string, any>[], defaultParamIt
|
|||
delete defaultParam.id;
|
||||
delete defaultParam.enable;
|
||||
const lastDataIsDefault = isEqual(lastData, defaultParam);
|
||||
let validParams: Record<string, any>[] = [];
|
||||
let validParams: (T & Record<string, any>)[];
|
||||
if (lastDataIsDefault) {
|
||||
// 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个
|
||||
validParams = params.slice(0, params.length - 1);
|
||||
|
|
|
@ -249,7 +249,6 @@
|
|||
return {
|
||||
...e,
|
||||
hideMoreAction: e.id === 'root',
|
||||
draggable: e.id !== 'root',
|
||||
};
|
||||
});
|
||||
rootModulesName.value = folderTree.value.map((e) => e.name || '');
|
||||
|
@ -381,6 +380,7 @@
|
|||
}
|
||||
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;
|
||||
|
@ -400,6 +400,10 @@
|
|||
dropPosition: number
|
||||
) {
|
||||
try {
|
||||
if (dragNode.id === 'root' || (dragNode.type === 'MODULE' && dropNode.id === 'root')) {
|
||||
// 根节点不可拖拽;模块不可拖拽到根节点
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
if (dragNode.type === 'MODULE') {
|
||||
await moveDebugModule({
|
||||
|
@ -411,8 +415,8 @@
|
|||
await dragDebug({
|
||||
projectId: appStore.currentProjectId,
|
||||
moveMode: dropPositionMap[dropPosition],
|
||||
moveId: dropNode.id,
|
||||
targetId: dragNode.id,
|
||||
moveId: dragNode.id,
|
||||
targetId: dropNode.id,
|
||||
moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id, // 释放节点是 API,则传入它所属模块id;模块的话直接是模块id
|
||||
});
|
||||
}
|
||||
|
|
|
@ -103,12 +103,12 @@
|
|||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import debug, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
|
||||
import {
|
||||
addDebug,
|
||||
executeDebug,
|
||||
getDebugDetail,
|
||||
getTransferOptions,
|
||||
localExecuteApiDebug,
|
||||
transferFile,
|
||||
updateDebug,
|
||||
uploadTempFile,
|
||||
|
|
|
@ -86,11 +86,10 @@ export default {
|
|||
'apiTestDebug.quoteSource': 'Reference data source',
|
||||
'apiTestDebug.sourceList': 'Data source list',
|
||||
'apiTestDebug.quoteSourcePlaceholder': 'Please select a data source',
|
||||
'apiTestDebug.storageType': 'Storage method',
|
||||
'apiTestDebug.storageTypeTip1':
|
||||
'Store by column: Specify the names of columns extracted from the database result set; multiple columns can be separated by ","',
|
||||
'apiTestDebug.storageTypeTip2':
|
||||
'Store by result: Save the entire result set as a variable instead of saving each column value as a separate variable',
|
||||
'apiTestDebug.storageColTip':
|
||||
'Specify the names of columns extracted from the database result set; multiple columns can be separated by ","',
|
||||
'apiTestDebug.storageResultTip':
|
||||
'Save the entire result set as a variable instead of saving each column value as a separate variable',
|
||||
'apiTestDebug.storageByCol': 'Store by columns',
|
||||
'apiTestDebug.storageByColPlaceholder': 'For example, {a} is changed to {b}',
|
||||
'apiTestDebug.storageByResult': 'Store by result',
|
||||
|
|
|
@ -82,9 +82,8 @@ export default {
|
|||
'apiTestDebug.quoteSource': '引用数据源',
|
||||
'apiTestDebug.sourceList': '数据源列表',
|
||||
'apiTestDebug.quoteSourcePlaceholder': '请选择数据源',
|
||||
'apiTestDebug.storageType': '存储方式',
|
||||
'apiTestDebug.storageTypeTip1': '按列存储:指定从数据库结果集中提取的列的名称;多个列可以使用“,”分隔',
|
||||
'apiTestDebug.storageTypeTip2': '按结果存储:把整个结果集保存为一个变量,而不是将每个列的值保存为单独的变量',
|
||||
'apiTestDebug.storageColTip': '指定从数据库结果集中提取的列的名称;多个列可以使用“,”分隔',
|
||||
'apiTestDebug.storageResultTip': '把整个结果集保存为一个变量,而不是将每个列的值保存为单独的变量',
|
||||
'apiTestDebug.storageByCol': '按列存储',
|
||||
'apiTestDebug.storageByColPlaceholder': '如 {a} 改成 {b}',
|
||||
'apiTestDebug.storageByResult': '按结果存储',
|
||||
|
|
|
@ -1,325 +1,372 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="visible"
|
||||
width="100%"
|
||||
:popup-container="props.popupContainer"
|
||||
:closable="false"
|
||||
:ok-disabled="disabledConfirm"
|
||||
disabled-width-drag
|
||||
no-title
|
||||
@confirm="confirmImport"
|
||||
@cancel="cancelImport"
|
||||
>
|
||||
<template #title> </template>
|
||||
<div class="flex items-center justify-between p-[12px_8px]">
|
||||
<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 value="file">{{ t('apiTestManagement.fileImport') }}</a-radio>
|
||||
<a-radio value="time">{{ t('apiTestManagement.timeImport') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div
|
||||
v-if="importType === 'file'"
|
||||
class="my-[16px] flex items-center gap-[16px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]"
|
||||
<div>
|
||||
<MsDrawer
|
||||
v-model:visible="visible"
|
||||
width="100%"
|
||||
:popup-container="props.popupContainer"
|
||||
:closable="false"
|
||||
:ok-disabled="disabledConfirm"
|
||||
:ok-text="t('common.import')"
|
||||
:ok-loading="importLoading"
|
||||
disabled-width-drag
|
||||
desc
|
||||
@confirm="confirmImport"
|
||||
@cancel="cancelImport"
|
||||
>
|
||||
<div
|
||||
v-for="item of importFormatList"
|
||||
:key="item.value"
|
||||
:class="`import-item ${importFormat === item.value ? 'import-item--active' : ''}`"
|
||||
@click="() => setActiveImportFormat(item.value)"
|
||||
>
|
||||
<div class="flex h-[24px] w-[24px] items-center justify-center rounded-[var(--border-radius-small)] bg-white">
|
||||
<MsIcon :type="item.icon" :class="`text-[${item.iconColor}]`" :size="18" />
|
||||
</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ item.name }}</div>
|
||||
<template #title> </template>
|
||||
<div class="flex items-center justify-between p-[12px_8px]">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('apiTestManagement.importApi') }}</div>
|
||||
<a-radio-group v-model:model-value="importForm.type" type="button">
|
||||
<a-radio :value="RequestImportType.API">{{ t('apiTestManagement.fileImport') }}</a-radio>
|
||||
<a-radio :value="RequestImportType.SCHEDULE">{{ t('apiTestManagement.timeImport') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<a-form ref="importFormRef" :model="importForm" layout="vertical">
|
||||
<template v-if="importType === 'file'">
|
||||
<a-form-item :label="t('apiTestManagement.belongModule')">
|
||||
<a-tree-select
|
||||
v-model:modelValue="importForm.module"
|
||||
:data="moduleTree"
|
||||
class="w-[436px]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
allow-search
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-[2px]">
|
||||
{{ t('apiTestManagement.importMode') }}
|
||||
<a-tooltip position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('apiTestManagement.importModeTip1') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip2') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip3') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip4') }}</div>
|
||||
<div class="h-[22px] w-full"></div>
|
||||
<div>{{ t('apiTestManagement.importModeTip5') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip6') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip7') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<a-select v-model:model-value="importForm.mode" class="w-[240px]">
|
||||
<a-option value="cover">{{ t('apiTestManagement.cover') }}</a-option>
|
||||
<a-option value="uncover">{{ t('apiTestManagement.uncover') }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false">
|
||||
<a-collapse-item :key="1">
|
||||
<template #header>
|
||||
<MsButton
|
||||
type="text"
|
||||
@click="() => (moreSettingActive.length > 0 ? (moreSettingActive = []) : (moreSettingActive = [1]))"
|
||||
>
|
||||
{{ t('apiTestDebug.moreSetting') }}
|
||||
<icon-down v-if="moreSettingActive.length > 0" class="text-rgb(var(--primary-5))" />
|
||||
<icon-right v-else class="text-rgb(var(--primary-5))" />
|
||||
</MsButton>
|
||||
</template>
|
||||
<div class="mt-[16px]">
|
||||
<a-checkbox v-model:model-value="importForm.syncImportCase" class="mr-[24px]">
|
||||
{{ t('apiTestManagement.syncImportCase') }}
|
||||
</a-checkbox>
|
||||
<a-checkbox v-model:model-value="importForm.syncUpdateDirectory">
|
||||
{{ t('apiTestManagement.syncUpdateDirectory') }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
</a-collapse>
|
||||
<a-form-item :label="t('apiTestManagement.importType')" class="mt-[8px]">
|
||||
<a-radio-group v-model:model-value="importForm.importType" type="button">
|
||||
<a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio>
|
||||
<a-radio value="url">{{ t('apiTestManagement.urlImport') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<MsUpload
|
||||
v-if="importForm.importType === 'file'"
|
||||
v-model:file-list="importForm.file"
|
||||
accept="json"
|
||||
:auto-upload="false"
|
||||
draggable
|
||||
size-unit="MB"
|
||||
class="w-full"
|
||||
<div
|
||||
v-if="importType === 'file'"
|
||||
class="my-[16px] flex items-center gap-[16px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]"
|
||||
>
|
||||
<div
|
||||
v-for="item of platformList"
|
||||
:key="item.value"
|
||||
:class="`import-item ${importForm.platform === item.value ? 'import-item--active' : ''}`"
|
||||
@click="() => setActiveImportFormat(item.value)"
|
||||
>
|
||||
<template #subText>
|
||||
<div class="flex">
|
||||
{{ t('apiTestManagement.importSwaggerFileTip1') }}
|
||||
<div class="text-[rgb(var(--warning-6))]">{{ t('apiTestManagement.importSwaggerFileTip2') }}</div>
|
||||
{{ t('apiTestManagement.importSwaggerFileTip3') }}
|
||||
<div class="flex h-[24px] w-[24px] items-center justify-center rounded-[var(--border-radius-small)] bg-white">
|
||||
<MsIcon :type="item.icon" :class="`text-[${item.iconColor}]`" :size="18" />
|
||||
</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-form ref="importFormRef" :model="importForm" layout="vertical">
|
||||
<template v-if="importForm.type === RequestImportType.API">
|
||||
<a-form-item :label="t('apiTestManagement.belongModule')">
|
||||
<a-tree-select
|
||||
v-model:modelValue="importForm.moduleId"
|
||||
:data="moduleTree"
|
||||
class="w-[436px]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
allow-search
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-[2px]">
|
||||
{{ t('apiTestManagement.importMode') }}
|
||||
<a-tooltip position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('apiTestManagement.importModeTip1') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip2') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip3') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip4') }}</div>
|
||||
<div class="h-[22px] w-full"></div>
|
||||
<div>{{ t('apiTestManagement.importModeTip5') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip6') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip7') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<a-select v-model:model-value="importForm.coverData" class="w-[240px]">
|
||||
<a-option :value="true">{{ t('apiTestManagement.cover') }}</a-option>
|
||||
<a-option :value="false">{{ t('apiTestManagement.uncover') }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false">
|
||||
<a-collapse-item :key="1">
|
||||
<template #header>
|
||||
<MsButton
|
||||
type="text"
|
||||
@click="() => (moreSettingActive.length > 0 ? (moreSettingActive = []) : (moreSettingActive = [1]))"
|
||||
>
|
||||
{{ t('apiTestDebug.moreSetting') }}
|
||||
<icon-down v-if="moreSettingActive.length > 0" class="text-rgb(var(--primary-5))" />
|
||||
<icon-right v-else class="text-rgb(var(--primary-5))" />
|
||||
</MsButton>
|
||||
</template>
|
||||
<div class="mt-[16px]">
|
||||
<a-checkbox v-model:model-value="importForm.syncCase" class="mr-[24px]">
|
||||
{{ t('apiTestManagement.syncImportCase') }}
|
||||
</a-checkbox>
|
||||
<a-checkbox v-model:model-value="importForm.coverModule">
|
||||
{{ t('apiTestManagement.syncUpdateDirectory') }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
</a-collapse>
|
||||
<a-form-item :label="t('apiTestManagement.importType')" class="mt-[8px]">
|
||||
<a-radio-group v-model:model-value="importType" type="button">
|
||||
<a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio>
|
||||
<a-radio value="swaggerUrl">{{ t('apiTestManagement.urlImport') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<MsUpload
|
||||
v-if="importType === 'file'"
|
||||
v-model:file-list="fileList"
|
||||
accept="json"
|
||||
:auto-upload="false"
|
||||
draggable
|
||||
size-unit="MB"
|
||||
class="w-full"
|
||||
>
|
||||
<template #subText>
|
||||
<div class="flex">
|
||||
{{ t('apiTestManagement.importSwaggerFileTip1') }}
|
||||
<div class="text-[rgb(var(--warning-6))]">{{ t('apiTestManagement.importSwaggerFileTip2') }}</div>
|
||||
{{ t('apiTestManagement.importSwaggerFileTip3') }}
|
||||
</div>
|
||||
</template>
|
||||
</MsUpload>
|
||||
<template v-else>
|
||||
<a-form-item
|
||||
field="swaggerUrl"
|
||||
label="SwaggerURL"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="importForm.swaggerUrl"
|
||||
:placeholder="t('apiTestManagement.urlImportPlaceholder')"
|
||||
class="w-[700px]"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<div class="mb-[16px] flex items-center gap-[8px]">
|
||||
<a-switch v-model:model-value="importForm.authSwitch" type="line" size="small"></a-switch>
|
||||
{{ t('apiTestManagement.basicAuth') }}
|
||||
</div>
|
||||
<template v-if="importForm.authSwitch">
|
||||
<a-form-item
|
||||
field="authUsername"
|
||||
:label="t('apiTestManagement.account')"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="importForm.authUsername"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
class="w-[500px]"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="authPassword"
|
||||
:label="t('apiTestManagement.password')"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:model-value="importForm.authPassword"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
class="w-[500px]"
|
||||
autocomplete="new-password"
|
||||
allow-clear
|
||||
></a-input-password>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</MsUpload>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item
|
||||
field="url"
|
||||
field="name"
|
||||
:label="t('apiTestManagement.taskName')"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.taskNameRequired') }]"
|
||||
>
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
<a-input
|
||||
v-model:model-value="importForm.name"
|
||||
:placeholder="t('apiTestManagement.taskNamePlaceholder')"
|
||||
:max-length="255"
|
||||
class="flex-1"
|
||||
></a-input>
|
||||
<MsButton type="text" @click="taskDrawerVisible = true">{{
|
||||
t('apiTestManagement.timeTaskList')
|
||||
}}</MsButton>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="swaggerUrl"
|
||||
label="SwaggerURL"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="importForm.url"
|
||||
v-model:model-value="importForm.swaggerUrl"
|
||||
:placeholder="t('apiTestManagement.urlImportPlaceholder')"
|
||||
class="w-[700px]"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<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') }}
|
||||
</div>
|
||||
<template v-if="importForm.basicAuth">
|
||||
<template v-if="importForm.authSwitch">
|
||||
<a-form-item
|
||||
field="account"
|
||||
field="authUsername"
|
||||
:label="t('apiTestManagement.account')"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="importForm.account"
|
||||
v-model:model-value="importForm.authUsername"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
class="w-[500px]"
|
||||
allow-clear
|
||||
></a-input>
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
field="authPassword"
|
||||
:label="t('apiTestManagement.password')"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
|
||||
asterisk-position="end"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:model-value="importForm.password"
|
||||
v-model:model-value="importForm.authPassword"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
class="w-[500px]"
|
||||
autocomplete="new-password"
|
||||
allow-clear
|
||||
></a-input-password>
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item
|
||||
field="taskName"
|
||||
:label="t('apiTestManagement.taskName')"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.taskNameRequired') }]"
|
||||
>
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
<a-input
|
||||
v-model:model-value="importForm.taskName"
|
||||
:placeholder="t('apiTestManagement.taskNamePlaceholder')"
|
||||
:max-length="255"
|
||||
class="flex-1"
|
||||
></a-input>
|
||||
<MsButton type="text">{{ t('apiTestManagement.timeTaskList') }}</MsButton>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="url"
|
||||
label="SwaggerURL"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="importForm.url"
|
||||
:placeholder="t('apiTestManagement.urlImportPlaceholder')"
|
||||
class="w-[700px]"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<div class="mb-[16px] flex items-center gap-[8px]">
|
||||
<a-switch v-model:model-value="importForm.basicAuth" type="line" size="small"></a-switch>
|
||||
{{ t('apiTestManagement.basicAuth') }}
|
||||
</div>
|
||||
<template v-if="importForm.basicAuth">
|
||||
<a-form-item
|
||||
field="account"
|
||||
:label="t('apiTestManagement.account')"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="importForm.account"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
class="w-[500px]"
|
||||
allow-clear
|
||||
<a-form-item :label="t('apiTestManagement.belongModule')">
|
||||
<a-tree-select
|
||||
v-model:modelValue="importForm.moduleId"
|
||||
:data="moduleTree"
|
||||
class="w-[436px]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
allow-search
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
:label="t('apiTestManagement.password')"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
|
||||
asterisk-position="end"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:model-value="importForm.password"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
class="w-[500px]"
|
||||
autocomplete="new-password"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item :label="t('apiTestManagement.belongModule')">
|
||||
<a-tree-select
|
||||
v-model:modelValue="importForm.module"
|
||||
:data="moduleTree"
|
||||
class="w-[436px]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
allow-search
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-[2px]">
|
||||
{{ t('apiTestManagement.importMode') }}
|
||||
<a-tooltip position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('apiTestManagement.importModeTip1') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip2') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip3') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip4') }}</div>
|
||||
<div class="h-[22px] w-full"></div>
|
||||
<div>{{ t('apiTestManagement.importModeTip5') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip6') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip7') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<a-select v-model:model-value="importForm.mode" class="w-[240px]">
|
||||
<a-option value="cover">{{ t('apiTestManagement.cover') }}</a-option>
|
||||
<a-option value="uncover">{{ t('apiTestManagement.uncover') }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiTestManagement.syncFrequency')">
|
||||
<a-select v-model:model-value="importForm.syncFrequency" class="w-[240px]">
|
||||
<template #label="{ data }">
|
||||
<div class="flex items-center">
|
||||
{{ data.value }}
|
||||
<div class="ml-[4px] text-[var(--color-text-4)]">{{ data.label.split('?')[1] }}</div>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-[2px]">
|
||||
{{ t('apiTestManagement.importMode') }}
|
||||
<a-tooltip position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('apiTestManagement.importModeTip1') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip2') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip3') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip4') }}</div>
|
||||
<div class="h-[22px] w-full"></div>
|
||||
<div>{{ t('apiTestManagement.importModeTip5') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip6') }}</div>
|
||||
<div>{{ t('apiTestManagement.importModeTip7') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value" class="block">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
{{ item.value }}
|
||||
<div class="ml-[4px] text-[var(--color-text-4)]">{{ item.label }}</div>
|
||||
</div>
|
||||
</a-option>
|
||||
<template #footer>
|
||||
<a-select v-model:model-value="importForm.coverData" class="w-[240px]">
|
||||
<a-option :value="true">{{ t('apiTestManagement.cover') }}</a-option>
|
||||
<a-option :value="false">{{ t('apiTestManagement.uncover') }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiTestManagement.syncFrequency')">
|
||||
<a-select v-model:model-value="cronValue" class="w-[240px]">
|
||||
<template #label="{ data }">
|
||||
<div class="flex items-center">
|
||||
{{ data.value }}
|
||||
<div class="ml-[4px] text-[var(--color-text-4)]">{{ data.label.split('?')[1] }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value" class="block">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
{{ item.value }}
|
||||
<div class="ml-[4px] text-[var(--color-text-4)]">{{ item.label }}</div>
|
||||
</div>
|
||||
</a-option>
|
||||
<!-- TODO:第一版不做自定义 -->
|
||||
<!-- <template #footer>
|
||||
<div class="flex items-center p-[4px_8px]">
|
||||
<MsButton type="text">{{ t('apiTestManagement.customFrequency') }}</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</MsDrawer>
|
||||
</template> -->
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/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 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 useAppStore from '@/store/modules/app';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import { mapTree } from '@/utils';
|
||||
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { RequestImportFormat } from '@/enums/apiEnum';
|
||||
import type { ImportApiDefinitionParams, ImportApiDefinitionRequest } from '@/models/apiTest/management';
|
||||
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<{
|
||||
visible: boolean;
|
||||
moduleTree: ModuleTreeNode[];
|
||||
popupContainer?: string;
|
||||
}>();
|
||||
const emit = defineEmits(['update:visible']);
|
||||
const emit = defineEmits(['update:visible', 'done']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const visible = useVModel(props, 'visible', emit);
|
||||
const importType = ref<'file' | 'time'>('file');
|
||||
const importFormat = ref<keyof typeof RequestImportFormat>('SWAGGER');
|
||||
const importFormatList = [
|
||||
const platformList = [
|
||||
{
|
||||
name: 'Swagger',
|
||||
value: RequestImportFormat.SWAGGER,
|
||||
|
@ -327,64 +374,232 @@
|
|||
iconColor: 'rgb(var(--success-7))',
|
||||
},
|
||||
];
|
||||
|
||||
function setActiveImportFormat(format: RequestImportFormat) {
|
||||
importFormat.value = format;
|
||||
}
|
||||
|
||||
const defaultForm = {
|
||||
taskName: '',
|
||||
module: 'root',
|
||||
mode: 'cover',
|
||||
syncImportCase: true,
|
||||
syncUpdateDirectory: false,
|
||||
importType: 'file',
|
||||
file: [],
|
||||
url: '',
|
||||
basicAuth: false,
|
||||
account: '',
|
||||
password: '',
|
||||
syncFrequency: '0 0 0/1 * ?',
|
||||
const fileList = ref<MsFileItem[]>([]);
|
||||
const defaultForm: ImportApiDefinitionRequest = {
|
||||
platform: RequestImportFormat.SWAGGER,
|
||||
name: '',
|
||||
moduleId: 'root',
|
||||
coverData: true,
|
||||
syncCase: true,
|
||||
coverModule: false,
|
||||
swaggerUrl: '',
|
||||
authSwitch: false,
|
||||
authUsername: '',
|
||||
authPassword: '',
|
||||
type: RequestImportType.API,
|
||||
userId: userStore.id || '',
|
||||
protocol: 'HTTP',
|
||||
projectId: appStore.currentProjectId,
|
||||
};
|
||||
const importForm = ref({ ...defaultForm });
|
||||
const importFormRef = ref<FormInstance>();
|
||||
const moreSettingActive = ref<number[]>([]);
|
||||
const disabledConfirm = computed(() => {
|
||||
if (importType.value === 'file') {
|
||||
if (importForm.value.importType === 'file') {
|
||||
return !importForm.value.file.length;
|
||||
if (importForm.value.type === RequestImportType.API) {
|
||||
if (importType.value === 'file') {
|
||||
return !fileList.value.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 syncFrequencyOptions = [
|
||||
{ label: t('apiTestManagement.timeTaskHour'), value: '0 0 0/1 * ?' },
|
||||
{ label: t('apiTestManagement.timeTaskSixHour'), value: '0 0 0/6 * ?' },
|
||||
{ label: t('apiTestManagement.timeTaskTwelveHour'), value: '0 0 0/12 * ?' },
|
||||
{ label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * ?' },
|
||||
{ label: t('apiTestManagement.timeTaskHour'), value: '0 0 0/1 * * ? ' },
|
||||
{ label: t('apiTestManagement.timeTaskSixHour'), value: '0 0 0/6 * * ?' },
|
||||
{ label: t('apiTestManagement.timeTaskTwelveHour'), value: '0 0 0/12 * * ?' },
|
||||
{ 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() {
|
||||
visible.value = false;
|
||||
importForm.value = { ...defaultForm };
|
||||
importFormRef.value?.resetFields();
|
||||
importType.value = 'file';
|
||||
fileList.value = [];
|
||||
moreSettingActive.value = [];
|
||||
}
|
||||
|
||||
async function importDefinitionByFile() {
|
||||
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'));
|
||||
emit('done');
|
||||
cancelImport();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
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(async (errors) => {
|
||||
importFormRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
Message.success(t('common.importSuccess'));
|
||||
cancelImport();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<template>
|
||||
<div :class="['p-[16px_22px]', props.class]">
|
||||
<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]">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
|
@ -37,7 +33,7 @@
|
|||
trigger="click"
|
||||
@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) }}
|
||||
<icon-down :class="methodFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
|
||||
</MsButton>
|
||||
|
@ -81,12 +77,22 @@
|
|||
<MsButton type="text" @click="openApiTab(record)">{{ record.num }}</MsButton>
|
||||
</template>
|
||||
<template #method="{ record }">
|
||||
<apiMethodName :method="record.method" is-tag />
|
||||
<a-select
|
||||
v-model:model-value="record.method"
|
||||
class="param-input w-full"
|
||||
@change="() => handleMethodChange(record)"
|
||||
>
|
||||
<template #label>
|
||||
<apiMethodName :method="record.method" is-tag />
|
||||
</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 }">
|
||||
<a-select
|
||||
v-model:model-value="record.status"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
@change="() => handleStatusChange(record)"
|
||||
>
|
||||
|
@ -103,7 +109,7 @@
|
|||
{{ t('apiTestManagement.execute') }}
|
||||
</MsButton>
|
||||
<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') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
|
@ -136,12 +142,14 @@
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="batchForm.attr === 'tag'"
|
||||
v-if="batchForm.attr === 'tags'"
|
||||
field="values"
|
||||
:label="t('apiTestManagement.batchUpdate')"
|
||||
:validate-trigger="['blur', 'input']"
|
||||
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
|
||||
asterisk-position="end"
|
||||
class="mb-0"
|
||||
required
|
||||
>
|
||||
<MsTagsInput
|
||||
v-model:model-value="batchForm.values"
|
||||
|
@ -159,11 +167,7 @@
|
|||
asterisk-position="end"
|
||||
class="mb-0"
|
||||
>
|
||||
<apiMethodSelect
|
||||
v-if="batchForm.attr === 'type'"
|
||||
v-model:model-value="batchForm.value"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<apiMethodSelect v-if="batchForm.attr === 'method'" v-model:model-value="batchForm.value" />
|
||||
<a-select
|
||||
v-else
|
||||
v-model="batchForm.value"
|
||||
|
@ -218,7 +222,7 @@
|
|||
<moduleTree
|
||||
v-if="moveModalVisible"
|
||||
:is-expand-all="true"
|
||||
is-modal
|
||||
:is-modal="true"
|
||||
:active-module="props.activeModule"
|
||||
@folder-node-select="folderNodeSelect"
|
||||
/>
|
||||
|
@ -241,7 +245,14 @@
|
|||
import apiStatus from '@/views/api-test/components/apiStatus.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 useModal from '@/hooks/useModal';
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
|
@ -259,39 +270,16 @@
|
|||
readOnly?: boolean; // 是否是只读模式
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'init', params: any): void;
|
||||
(e: 'change'): void;
|
||||
(e: 'openApiTab', record: ApiDefinitionDetail): void;
|
||||
(e: 'openCopyApiTab', record: ApiDefinitionDetail): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
function handleActiveDebugChange() {
|
||||
emit('change');
|
||||
}
|
||||
|
||||
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 folderTreePathMap = inject('folderTreePathMap');
|
||||
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||
const keyword = ref('');
|
||||
|
||||
let columns: MsTableColumn = [
|
||||
|
@ -322,7 +310,7 @@
|
|||
dataIndex: 'method',
|
||||
slotName: 'method',
|
||||
titleSlotName: 'methodFilter',
|
||||
width: 120,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'apiTestManagement.apiStatus',
|
||||
|
@ -333,7 +321,6 @@
|
|||
},
|
||||
{
|
||||
title: 'apiTestManagement.path',
|
||||
slotName: 'path',
|
||||
dataIndex: 'path',
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
|
@ -386,19 +373,21 @@
|
|||
selectable: true,
|
||||
showSelectAll: !props.readOnly,
|
||||
draggable: props.readOnly ? undefined : { type: 'handle', width: 32 },
|
||||
heightUsed: 374,
|
||||
},
|
||||
(item) => ({
|
||||
...item,
|
||||
fullPath: folderTreePathMap?.[item.moduleId],
|
||||
createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
})
|
||||
);
|
||||
const batchActions = {
|
||||
baseAction: [
|
||||
{
|
||||
label: 'common.export',
|
||||
eventTag: 'export',
|
||||
},
|
||||
// {
|
||||
// label: 'common.export',
|
||||
// eventTag: 'export',
|
||||
// },
|
||||
{
|
||||
label: 'common.edit',
|
||||
eventTag: 'edit',
|
||||
|
@ -432,10 +421,6 @@
|
|||
if (props.activeModule === 'all') {
|
||||
return [];
|
||||
}
|
||||
if (showSubdirectory.value) {
|
||||
// 显示子目录接口
|
||||
return [props.activeModule, ...props.offspringIds];
|
||||
}
|
||||
return [props.activeModule];
|
||||
});
|
||||
const tableQueryParams = ref<any>();
|
||||
|
@ -444,9 +429,8 @@
|
|||
keyword: keyword.value,
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: moduleIds.value,
|
||||
env: checkedEnv.value,
|
||||
protocol: props.protocol,
|
||||
filter: { status: statusFilters.value, type: methodFilters.value },
|
||||
filter: { status: statusFilters.value, method: methodFilters.value },
|
||||
};
|
||||
setLoadListParams(params);
|
||||
loadList();
|
||||
|
@ -455,14 +439,12 @@
|
|||
current: propsRes.value.msPagination?.current,
|
||||
pageSize: propsRes.value.msPagination?.pageSize,
|
||||
};
|
||||
emit('init', {
|
||||
...tableQueryParams.value,
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.activeModule,
|
||||
() => {
|
||||
resetSelector();
|
||||
loadApiList();
|
||||
}
|
||||
);
|
||||
|
@ -470,6 +452,7 @@
|
|||
watch(
|
||||
() => props.protocol,
|
||||
() => {
|
||||
resetSelector();
|
||||
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) {
|
||||
try {
|
||||
await updateDefinition({
|
||||
id: record.id,
|
||||
status: record.status,
|
||||
});
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -493,16 +493,6 @@
|
|||
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 batchParams = ref<BatchActionQueryParams>({
|
||||
selectedIds: [],
|
||||
|
@ -536,20 +526,25 @@
|
|||
onBeforeOk: async () => {
|
||||
try {
|
||||
if (isBatch) {
|
||||
// await batchDeleteDefinition({
|
||||
// selectIds,
|
||||
// selectAll: !!params?.selectAll,
|
||||
// excludeIds: params?.excludeIds || [],
|
||||
// condition: { keyword: keyword.value },
|
||||
// projectId: appStore.currentProjectId,
|
||||
// moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||
// });
|
||||
await batchDeleteDefinition({
|
||||
selectIds,
|
||||
selectAll: !!params?.selectAll,
|
||||
excludeIds: params?.excludeIds || [],
|
||||
condition: { keyword: keyword.value },
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||
deleteAll: true,
|
||||
protocol: props.protocol,
|
||||
});
|
||||
} else {
|
||||
await deleteDefinition(record?.id as string);
|
||||
}
|
||||
Message.success(t('common.deleteSuccess'));
|
||||
resetSelector();
|
||||
loadList();
|
||||
if (typeof refreshModuleTree === 'function') {
|
||||
refreshModuleTree();
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -588,20 +583,26 @@
|
|||
value: '',
|
||||
values: [],
|
||||
});
|
||||
const attrOptions = [
|
||||
const fullAttrs = [
|
||||
{
|
||||
name: 'apiTestManagement.apiStatus',
|
||||
value: 'status',
|
||||
},
|
||||
{
|
||||
name: 'apiTestManagement.apiType',
|
||||
value: 'type',
|
||||
value: 'method',
|
||||
},
|
||||
{
|
||||
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(() => {
|
||||
switch (batchForm.value.attr) {
|
||||
case 'status':
|
||||
|
@ -643,6 +644,17 @@
|
|||
if (!errors) {
|
||||
try {
|
||||
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'));
|
||||
cancelBatch();
|
||||
resetSelector();
|
||||
|
@ -669,16 +681,17 @@
|
|||
async function handleApiMove() {
|
||||
try {
|
||||
batchMoveApiLoading.value = true;
|
||||
// await batchMoveFile({
|
||||
// selectIds: isBatchMove.value ? batchParams.value?.selectedIds || [] : [activeApi.value?.id || ''],
|
||||
// selectAll: !!batchParams.value?.selectAll,
|
||||
// excludeIds: batchParams.value?.excludeIds || [],
|
||||
// condition: { keyword: keyword.value },
|
||||
// projectId: appStore.currentProjectId,
|
||||
// moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||
// moveModuleId: selectedModuleKeys.value[0],
|
||||
// });
|
||||
Message.success(t('apiTestManagement.batchMoveSuccess'));
|
||||
await batchMoveDefinition({
|
||||
selectIds: isBatchMove.value ? batchParams.value?.selectedIds || [] : [activeApi.value?.id || ''],
|
||||
selectAll: !!batchParams.value?.selectAll,
|
||||
excludeIds: batchParams.value?.excludeIds || [],
|
||||
condition: { keyword: keyword.value },
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||
moduleId: selectedModuleKeys.value[0],
|
||||
protocol: props.protocol,
|
||||
});
|
||||
Message.success(t('common.batchMoveSuccess'));
|
||||
if (isBatchMove.value) {
|
||||
tableSelected.value = [];
|
||||
isBatchMove.value = false;
|
||||
|
@ -687,7 +700,9 @@
|
|||
}
|
||||
loadList();
|
||||
resetSelector();
|
||||
emitTableParams();
|
||||
if (typeof refreshModuleTree === 'function') {
|
||||
refreshModuleTree();
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -735,6 +750,10 @@
|
|||
emit('openApiTab', record);
|
||||
}
|
||||
|
||||
function copyDefinition(record: ApiDefinitionDetail) {
|
||||
emit('openCopyApiTab', record);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
loadApiList,
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
:offspring-ids="props.offspringIds"
|
||||
:protocol="props.protocol"
|
||||
@open-api-tab="openApiTab"
|
||||
@open-copy-api-tab="openApiTab($event, true)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
|
||||
|
@ -49,7 +50,7 @@
|
|||
hide-response-layout-switch
|
||||
:create-api="addDefinition"
|
||||
:update-api="updateDefinition"
|
||||
:execute-api="executeDebug"
|
||||
:execute-api="debugDefinition"
|
||||
:local-execute-api="localExecuteApiDebug"
|
||||
:permission-map="{
|
||||
execute: 'PROJECT_API_DEFINITION:READ+EXECUTE',
|
||||
|
@ -196,9 +197,10 @@
|
|||
import apiMethodName from '@/views/api-test/components/apiMethodName.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 {
|
||||
addDefinition,
|
||||
debugDefinition,
|
||||
getDefinitionDetail,
|
||||
getTransferOptions,
|
||||
transferFile,
|
||||
|
@ -411,9 +413,9 @@
|
|||
}
|
||||
|
||||
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);
|
||||
if (isLoadedTabIndex > -1) {
|
||||
if (isLoadedTabIndex > -1 && !isCopy) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
|
||||
return;
|
||||
|
@ -421,14 +423,17 @@
|
|||
try {
|
||||
loading.value = true;
|
||||
const res = await getDefinitionDetail(apiInfo.id);
|
||||
const name = isCopy ? `${res.name}-copy` : res.name;
|
||||
addApiTab({
|
||||
label: apiInfo.name,
|
||||
label: name,
|
||||
...res.request,
|
||||
...res,
|
||||
response: cloneDeep(defaultResponse),
|
||||
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
|
||||
url: res.path,
|
||||
name: res.name, // request里面还有个name但是是null
|
||||
isNew: false,
|
||||
name, // request里面还有个name但是是null
|
||||
isNew: isCopy,
|
||||
unSaved: isCopy,
|
||||
});
|
||||
nextTick(() => {
|
||||
// 等待内容渲染出来再隐藏loading
|
||||
|
@ -483,7 +488,7 @@
|
|||
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
|
||||
const activeApiTabFormRef = ref<FormInstance>();
|
||||
|
||||
function handleSave(params: ApiDefinitionCreateParams | ApiDefinitionUpdateParams) {
|
||||
function handleSave(params: ApiDefinitionCreateParams) {
|
||||
activeApiTabFormRef.value?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
splitBoxRef.value?.expand();
|
||||
|
@ -521,9 +526,14 @@
|
|||
console.log(params);
|
||||
}
|
||||
|
||||
function refreshTable() {
|
||||
apiTableRef.value?.loadApiList();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openApiTab,
|
||||
addApiTab,
|
||||
refreshTable,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -28,12 +28,14 @@
|
|||
</template>
|
||||
</a-button>
|
||||
<MsSelect
|
||||
v-model:model-value="checkedEnv"
|
||||
v-model:model-value="currentEnv"
|
||||
mode="static"
|
||||
:options="envOptions"
|
||||
class="!w-[150px]"
|
||||
:search-keys="['label']"
|
||||
:loading="envLoading"
|
||||
allow-search
|
||||
@change="initEnvironment"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -41,11 +43,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import api from './api/index.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';
|
||||
|
||||
|
@ -56,7 +61,7 @@
|
|||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const activeTab = ref('api');
|
||||
const apiRef = ref<InstanceType<typeof api>>();
|
||||
|
@ -69,28 +74,52 @@
|
|||
}
|
||||
}
|
||||
|
||||
const checkedEnv = ref('DEV');
|
||||
const envOptions = ref([
|
||||
{
|
||||
label: 'DEV',
|
||||
value: 'DEV',
|
||||
},
|
||||
{
|
||||
label: 'TEST',
|
||||
value: 'TEST',
|
||||
},
|
||||
{
|
||||
label: 'PRE',
|
||||
value: 'PRE',
|
||||
},
|
||||
{
|
||||
label: 'PROD',
|
||||
value: 'PROD',
|
||||
},
|
||||
]);
|
||||
const currentEnv = ref('');
|
||||
const currentEnvConfig = ref({});
|
||||
const envLoading = ref(false);
|
||||
const envOptions = ref<SelectOptionData[]>([]);
|
||||
|
||||
async function initEnvironment() {
|
||||
try {
|
||||
currentEnvConfig.value = await getEnvironment(currentEnv.value);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function initEnvList() {
|
||||
try {
|
||||
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({
|
||||
newTab,
|
||||
refreshApiTable,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,69 +1,80 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-select
|
||||
v-if="!props.readOnly"
|
||||
v-model:model-value="moduleProtocol"
|
||||
:options="moduleProtocolOptions"
|
||||
class="mb-[8px]"
|
||||
@change="() => handleProtocolChange()"
|
||||
/>
|
||||
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
||||
<a-dropdown v-if="!props.readOnly" @select="handleSelect">
|
||||
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div class="folder" @click="setActiveFolder('all')">
|
||||
<div :class="allFolderClass">
|
||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div>
|
||||
<div class="folder-count">({{ allFileCount }})</div>
|
||||
<template v-if="!props.isModal">
|
||||
<a-select
|
||||
v-if="!props.readOnly"
|
||||
v-model:model-value="moduleProtocol"
|
||||
:options="moduleProtocolOptions"
|
||||
class="mb-[8px]"
|
||||
@change="() => handleProtocolChange()"
|
||||
/>
|
||||
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
||||
<a-dropdown v-if="!props.readOnly" @select="handleSelect">
|
||||
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||
<a-doption v-if="moduleProtocol === 'HTTP'" value="import">
|
||||
{{ t('apiTestManagement.importApi') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center">
|
||||
<a-tooltip
|
||||
v-if="!props.readOnly"
|
||||
:content="isExpandApi ? t('apiTestManagement.collapseApi') : t('apiTestManagement.expandApi')"
|
||||
>
|
||||
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeApiExpand">
|
||||
<MsIcon :type="isExpandApi ? 'icon-icon_collapse_interface' : 'icon-icon_expand_interface'" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
|
||||
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
|
||||
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<template v-if="!props.readOnly">
|
||||
<a-dropdown @select="handleSelect">
|
||||
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||
<MsIcon
|
||||
type="icon-icon_create_planarity"
|
||||
size="18"
|
||||
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
|
||||
/>
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||
<a-doption value="addModule">{{ t('apiTestManagement.addSubModule') }}</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<popConfirm
|
||||
mode="add"
|
||||
:all-names="rootModulesName"
|
||||
parent-id="NONE"
|
||||
:add-module-api="addModule"
|
||||
@add-finish="initModules"
|
||||
<div class="folder" @click="setActiveFolder('all')">
|
||||
<div :class="allFolderClass">
|
||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div>
|
||||
<div class="folder-count">({{ allFileCount }})</div>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center">
|
||||
<a-tooltip
|
||||
v-if="!props.readOnly"
|
||||
:content="isExpandApi ? t('apiTestManagement.collapseApi') : t('apiTestManagement.expandApi')"
|
||||
>
|
||||
<span id="addModulePopSpan"></span>
|
||||
</popConfirm>
|
||||
</template>
|
||||
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeApiExpand">
|
||||
<MsIcon :type="isExpandApi ? 'icon-icon_collapse_interface' : 'icon-icon_expand_interface'" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
|
||||
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
|
||||
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<template v-if="!props.readOnly">
|
||||
<a-dropdown @select="handleSelect">
|
||||
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||
<MsIcon
|
||||
type="icon-icon_create_planarity"
|
||||
size="18"
|
||||
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
|
||||
/>
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||
<a-doption value="addModule">{{ t('apiTestManagement.addSubModule') }}</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<popConfirm
|
||||
mode="add"
|
||||
:all-names="rootModulesName"
|
||||
parent-id="NONE"
|
||||
:add-module-api="addModule"
|
||||
@add-finish="initModules"
|
||||
>
|
||||
<span id="addModulePopSpan"></span>
|
||||
</popConfirm>
|
||||
</template>
|
||||
</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">
|
||||
<MsTree
|
||||
v-model:focus-node-key="focusNodeKey"
|
||||
|
@ -81,8 +92,9 @@
|
|||
children: 'children',
|
||||
count: 'count',
|
||||
}"
|
||||
:draggable="!props.readOnly"
|
||||
:draggable="!props.readOnly && !props.isModal"
|
||||
:filter-more-action-func="filterMoreActionFunc"
|
||||
:allow-drop="allowDrop"
|
||||
block-node
|
||||
title-tooltip-position="left"
|
||||
@select="folderNodeSelect"
|
||||
|
@ -99,12 +111,12 @@
|
|||
<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>
|
||||
<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="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>
|
||||
</template>
|
||||
<template v-if="!props.readOnly" #extra="nodeData">
|
||||
<template v-if="!props.readOnly && !props.isModal" #extra="nodeData">
|
||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'"
|
||||
|
@ -122,12 +134,13 @@
|
|||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
mode="rename"
|
||||
:node-type="nodeData.type"
|
||||
:parent-id="nodeData.id"
|
||||
:node-id="nodeData.id"
|
||||
:field-config="{ field: renameFolderTitle }"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:update-module-api="updateModule"
|
||||
:update-api-node-api="updateModule"
|
||||
:update-api-node-api="updateDefinition"
|
||||
@close="resetFocusNodeKey"
|
||||
@rename-finish="initModules"
|
||||
>
|
||||
|
@ -159,8 +172,11 @@
|
|||
getModuleTree,
|
||||
getModuleTreeOnlyModules,
|
||||
moveModule,
|
||||
sortDefinition,
|
||||
updateDefinition,
|
||||
updateModule,
|
||||
} from '@/api/modules/api-test/management';
|
||||
import { dropPositionMap } from '@/config/common';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
@ -174,9 +190,12 @@
|
|||
activeModule?: string | number; // 选中的节点 key
|
||||
readOnly?: boolean; // 是否是只读模式
|
||||
activeNodeId?: string | number; // 当前选中节点 id
|
||||
isModal?: boolean; // 是否弹窗模式,只读且只可见模块树
|
||||
}>(),
|
||||
{
|
||||
activeModule: 'all',
|
||||
readOnly: false,
|
||||
isModal: false,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode', 'changeProtocol']);
|
||||
|
@ -224,7 +243,7 @@
|
|||
}
|
||||
|
||||
const virtualListProps = computed(() => {
|
||||
if (props.readOnly) {
|
||||
if (props.readOnly || props.isModal) {
|
||||
return {
|
||||
height: 'calc(60vh - 190px)',
|
||||
threshold: 200,
|
||||
|
@ -335,28 +354,38 @@
|
|||
moduleIds: [],
|
||||
});
|
||||
}
|
||||
if (props.readOnly) {
|
||||
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
|
||||
const nodePathObj: Record<string, any> = {};
|
||||
if (props.readOnly || props.isModal) {
|
||||
folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
|
||||
// 拼接当前节点的完整路径
|
||||
nodePathObj[e.id] = {
|
||||
path: e.path,
|
||||
fullPath,
|
||||
};
|
||||
return {
|
||||
...e,
|
||||
hideMoreAction: true,
|
||||
draggable: false,
|
||||
disabled: e.id === selectedKeys.value[0],
|
||||
};
|
||||
});
|
||||
} else {
|
||||
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
|
||||
folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
|
||||
// 拼接当前节点的完整路径
|
||||
nodePathObj[e.id] = {
|
||||
path: e.path,
|
||||
fullPath,
|
||||
};
|
||||
return {
|
||||
...e,
|
||||
hideMoreAction: e.id === 'root',
|
||||
draggable: e.id !== 'root',
|
||||
disabled: e.id === selectedKeys.value[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
if (isSetDefaultKey) {
|
||||
selectedKeys.value = [folderTree.value[0].id];
|
||||
}
|
||||
emit('init', folderTree.value, moduleProtocol.value);
|
||||
emit('init', folderTree.value, moduleProtocol.value, nodePathObj);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -378,7 +407,8 @@
|
|||
return {
|
||||
...node,
|
||||
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) {
|
||||
|
@ -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 树数据
|
||||
|
@ -495,13 +538,27 @@
|
|||
dropNode: MsTreeNodeData,
|
||||
dropPosition: number
|
||||
) {
|
||||
if (dragNode.id === 'root' || (dragNode.type === 'MODULE' && dropNode.id === 'root')) {
|
||||
// 根节点不可拖拽;模块不可拖拽到根节点
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
await moveModule({
|
||||
dragNodeId: dragNode.id as string,
|
||||
dropNodeId: dropNode.id || '',
|
||||
dropPosition,
|
||||
});
|
||||
if (dragNode.type === 'MODULE') {
|
||||
await moveModule({
|
||||
dragNodeId: dragNode.id as string,
|
||||
dropNodeId: dropNode.id || '',
|
||||
dropPosition,
|
||||
});
|
||||
} else {
|
||||
await sortDefinition({
|
||||
projectId: appStore.currentProjectId,
|
||||
moveMode: dropPositionMap[dropPosition],
|
||||
moveId: dragNode.id,
|
||||
targetId: dropNode.id,
|
||||
moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id, // 释放节点是 API,则传入它所属模块id;模块的话直接是模块id
|
||||
});
|
||||
}
|
||||
Message.success(t('apiTestDebug.moduleMoveSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -567,4 +624,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
:deep(#root ~ .arco-tree-node-drag-icon) {
|
||||
@apply hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MsCard simple no-content-padding>
|
||||
<MsCard :min-width="1180" simple no-content-padding>
|
||||
<MsSplitBox :size="0.25" :max="0.5">
|
||||
<template #first>
|
||||
<div class="p-[24px]">
|
||||
|
@ -36,6 +36,7 @@
|
|||
v-model:visible="importDrawerVisible"
|
||||
:module-tree="folderTree"
|
||||
popup-container="#managementContainer"
|
||||
@done="handleImportDone"
|
||||
/>
|
||||
</div>
|
||||
<management
|
||||
|
@ -65,6 +66,7 @@
|
|||
|
||||
const activeModule = ref<string>('all');
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
const folderTreePathMap = ref<Record<string, any>>({});
|
||||
const importDrawerVisible = ref(false);
|
||||
const offspringIds = ref<string[]>([]);
|
||||
const protocol = ref('HTTP');
|
||||
|
@ -72,9 +74,10 @@
|
|||
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
|
||||
const managementRef = ref<InstanceType<typeof management>>();
|
||||
|
||||
function handleModuleInit(tree, _protocol: string) {
|
||||
function handleModuleInit(tree, _protocol: string, pathMap: Record<string, any>) {
|
||||
folderTree.value = tree;
|
||||
protocol.value = _protocol;
|
||||
folderTreePathMap.value = pathMap;
|
||||
}
|
||||
|
||||
function newApi() {
|
||||
|
@ -102,9 +105,15 @@
|
|||
moduleTreeRef.value?.refresh();
|
||||
}
|
||||
|
||||
/** 向子孙组件提供方法 */
|
||||
function handleImportDone() {
|
||||
refreshModuleTree();
|
||||
managementRef.value?.refreshApiTable();
|
||||
}
|
||||
|
||||
/** 向子孙组件提供方法和值 */
|
||||
provide('setActiveApi', setActiveApi);
|
||||
provide('refreshModuleTree', refreshModuleTree);
|
||||
provide('folderTreePathMap', folderTreePathMap.value);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -3,6 +3,15 @@ export default {
|
|||
'apiTestManagement.importApi': 'Import api',
|
||||
'apiTestManagement.fileImport': 'Import file',
|
||||
'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.allApi': 'All api',
|
||||
'apiTestManagement.searchTip': 'Please enter module/api name',
|
||||
|
|
|
@ -3,6 +3,15 @@ export default {
|
|||
'apiTestManagement.importApi': '导入接口',
|
||||
'apiTestManagement.fileImport': '文件导入',
|
||||
'apiTestManagement.timeImport': '定时导入',
|
||||
'apiTestManagement.timeTask': '定时任务',
|
||||
'apiTestManagement.name': '名称',
|
||||
'apiTestManagement.taskRunRule': '运行规则',
|
||||
'apiTestManagement.taskNextRunTime': '下次执行时间',
|
||||
'apiTestManagement.taskOperator': '操作人',
|
||||
'apiTestManagement.taskOperationTime': '操作时间',
|
||||
'apiTestManagement.createTaskSuccess': '创建定时导入任务成功',
|
||||
'apiTestManagement.enableTaskSuccess': '开启定时导入任务成功',
|
||||
'apiTestManagement.disableTaskSuccess': '关闭定时导入任务成功',
|
||||
'apiTestManagement.addSubModule': '添加子模块',
|
||||
'apiTestManagement.allApi': '全部接口',
|
||||
'apiTestManagement.searchTip': '请输入模块/接口名称',
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
<BugCaseTab
|
||||
v-else-if="activeTab === 'case'"
|
||||
:bug-id="detailInfo.id"
|
||||
@updateCaseSuccess="updateSuccess"
|
||||
@update-case-success="updateSuccess"
|
||||
/>
|
||||
|
||||
<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 type { MsPaginationI } from '@/components/pure/ms-table/type';
|
||||
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 MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
|
||||
import BugCaseTab from './bugCaseTab.vue';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
</template>
|
||||
|
||||
<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 { createOrUpdateComment, deleteComment, getCommentList } from '@/api/modules/bug-management/index';
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
import MSAvatar from '@/components/pure/ms-avatar/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 {
|
||||
|
|
Loading…
Reference in New Issue