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

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

View File

@ -142,7 +142,7 @@
"stylelint-less": "^1.0.8",
"stylelint-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",

View File

@ -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 });
}

View File

@ -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 });

View File

@ -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 });
}
/**

View File

@ -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'; // 获取接口测试环境详情

View File

@ -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'; // 获取接口调试详情

View File

@ -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'; // 接口定义-调试

View File

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

View File

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

View File

@ -160,7 +160,7 @@
import { APIKEY } from '@/models/user';
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) {

View File

@ -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);
});

View File

@ -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)) ||

View File

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

View File

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

View File

@ -69,12 +69,14 @@
const { t } = useI18n();
const innerModelValue = ref(props.modelValue);
const innerInputValue = ref(props.inputValue);
const innerInputValue = defineModel<string>('inputValue', {
default: '',
});
const tagsLength = ref(0); // tagstag
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;

View File

@ -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'));

View File

@ -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 {

View File

@ -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[]; // 提取参数列表
}
// 执行请求-前后置条件-等待时间处理器

View File

@ -1,4 +1,6 @@
import { ModuleTreeNode, TableQueryParams } from '../common';
import { RequestDefinitionStatus, RequestImportFormat, RequestImportType } from '@/enums/apiEnum';
import { BatchApiParams, ModuleTreeNode, TableQueryParams } from '../common';
import { ExecuteRequestParams, ResponseDefinition } from './common';
// 定义-自定义字段
@ -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;
}

View File

@ -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;

View File

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

View File

@ -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;
}

View File

@ -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');
}

View File

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

View File

@ -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];

View File

@ -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>

View File

@ -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;

View File

@ -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),
},
],
},

View File

@ -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),

View File

@ -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');
}
}

View File

@ -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'));

View File

@ -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);

View File

@ -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, // APIidid
});
}

View File

@ -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,

View File

@ -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',

View File

@ -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': '按结果存储',

View File

@ -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>

View File

@ -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,
});

View File

@ -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) {
// tabtab
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, // requestnamenull
isNew: false,
name, // requestnamenull
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>

View File

@ -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>

View File

@ -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, // APIidid
});
}
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>

View File

@ -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>

View File

@ -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',

View File

@ -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': '请输入模块/接口名称',

View File

@ -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';

View File

@ -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';

View File

@ -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 {