feat(接口管理): 接口定义-详情

This commit is contained in:
baiqi 2024-03-11 18:37:17 +08:00 committed by 刘瑞斌
parent 690b819470
commit da550b75f7
39 changed files with 1590 additions and 166 deletions

View File

@ -53,6 +53,7 @@ module.exports = {
'@typescript-eslint/no-unused-vars': 1,
'@typescript-eslint/no-empty-function': 1,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-duplicate-enum-values': 0,
'consistent-return': 'off',
'import/extensions': [
2,

View File

@ -92,8 +92,8 @@
"@types/lodash-es": "^4.17.9",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.1",
"@vitest/coverage-c8": "^0.31.4",

View File

@ -24,6 +24,7 @@ import {
MoveModuleUrl,
SortDefinitionUrl,
SwitchDefinitionScheduleUrl,
ToggleFollowDefinitionUrl,
TransferFileModuleOptionUrl,
TransferFileUrl,
UpdateDefinitionScheduleUrl,
@ -203,6 +204,11 @@ export function debugDefinition(data: ExecuteRequestParams) {
return MSR.post({ url: DebugDefinitionUrl, data });
}
// 关注/取消关注接口定义
export function toggleFollowDefinition(id: string | number) {
return MSR.get({ url: ToggleFollowDefinitionUrl, params: id });
}
/**
* Mock
*/

View File

@ -6,6 +6,9 @@ export const GetEnvModuleUrl = '/api/definition/module/env/tree'; // 获取环
export const GetModuleCountUrl = '/api/definition/module/count'; // 获取模块统计数量
export const AddModuleUrl = '/api/definition/module/add'; // 添加模块
export const DeleteModuleUrl = '/api/definition/module/delete'; // 删除模块
/**
*
*/
export const DefinitionPageUrl = '/api/definition/page'; // 接口定义列表
export const AddDefinitionUrl = '/api/definition/add'; // 添加接口定义
export const UpdateDefinitionUrl = '/api/definition/update'; // 更新接口定义
@ -13,9 +16,6 @@ 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 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'; // 接口定义拖拽
@ -30,3 +30,10 @@ 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'; // 接口定义-调试
export const ToggleFollowDefinitionUrl = '/api/definition/follow'; // 接口定义-关注/取消关注
/**
* Mock
*/
export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表
export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态
export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock

View File

@ -632,6 +632,8 @@
}
}
.ms-params-input:not(.arco-input-focus) {
@apply bg-transparent;
border-color: transparent;
&:not(:hover) {
.arco-input::placeholder {

View File

@ -0,0 +1,106 @@
<template>
<div class="ms-detail-card">
<div class="flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<a-tooltip :content="t(props.title)">
<div class="one-line-text flex-1 font-medium text-[var(--color-text-1)]">
{{ t(props.title) }}
</div>
</a-tooltip>
<slot name="titleAppend"></slot>
</div>
<div v-if="$slots.titleRight" class="flex items-center">
<slot name="titleRight"></slot>
</div>
</div>
<div class="ms-detail-card-desc">
<div
v-for="item of showingDescription"
:key="item.key"
class="flex w-[calc(100%/3)] items-center gap-[8px]"
:style="{ width: item.width }"
>
<div class="text-[var(--color-text-4)]">{{ t(item.locale) }}</div>
<MsTagGroup v-if="Array.isArray(item.value)" :tag-list="item.value" size="small" is-string-tag />
<slot v-else :name="item.key" :value="item.value">
<a-tooltip :content="item.value" :disabled="isEmpty(item.value)">
<div class="text-[var(--color-text-1)]">{{ item.value || '-' }}</div>
</a-tooltip>
</slot>
</div>
</div>
<MsButton type="text" class="more-btn" @click="toggleExpand">
<div v-if="isExpand" class="flex items-center gap-[4px]">
{{ t('msDetailCard.collapse') }}
<icon-up :size="14" />
</div>
<div v-else class="flex items-center gap-[4px]">
{{ t('msDetailCard.more') }}
<icon-down :size="14" />
</div>
</MsButton>
</div>
</template>
<script setup lang="ts">
import { isEmpty } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import { useI18n } from '@/hooks/useI18n';
export interface Description {
key: string;
locale: string;
value: string | string[];
width?: string;
}
const props = defineProps<{
title: string;
description: Description[];
simpleShowCount?: number; //
}>();
const { t } = useI18n();
const isExpand = ref(false);
function toggleExpand() {
isExpand.value = !isExpand.value;
}
const showingDescription = computed(() => {
if (isExpand.value) {
return props.description;
}
if (props.simpleShowCount && props.description.length > props.simpleShowCount) {
return props.description.slice(0, props.simpleShowCount);
}
return props.description;
});
</script>
<style lang="less" scoped>
.ms-detail-card {
@apply relative flex flex-col;
padding: 16px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
gap: 8px;
.ms-detail-card-desc {
@apply flex flex-wrap overflow-hidden; // TODO:
}
.more-btn {
@apply absolute;
bottom: 2px;
left: 50%;
font-size: 12px;
transform: translateX(-50%);
line-height: 16px;
}
}
</style>

View File

@ -0,0 +1,4 @@
export default {
'msDetailCard.more': 'Expand more',
'msDetailCard.collapse': 'Collapse',
};

View File

@ -0,0 +1,4 @@
export default {
'msDetailCard.more': '展开更多',
'msDetailCard.collapse': '收起',
};

View File

@ -2,17 +2,120 @@
<MsBaseTable
v-bind="propsRes"
:hoverable="false"
no-disable
is-simple-setting
:span-method="props.spanMethod"
:class="!props.selectable && !props.draggable ? 'ms-form-table-no-left-action' : ''"
v-on="propsEvent"
>
<!-- 展开行-->
<template #expand-icon="{ expanded, record }">
<div class="flex items-center gap-[2px] text-[var(--color-text-4)]">
<MsIcon :type="expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'" />
<div v-if="record.children">{{ record.children.length }}</div>
</div>
</template>
<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] || '-' }}
<a-tooltip
v-if="item.hasRequired"
:content="t(record.required ? 'msFormTable.paramRequired' : 'msFormTable.paramNotRequired')"
>
<MsButton
type="icon"
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
size="mini"
@click="toggleRequired(record, rowIndex, item)"
>
<div>*</div>
</MsButton>
</a-tooltip>
<a-input
v-if="item.inputType === 'input'"
v-model:model-value="record[item.dataIndex as string]"
:placeholder="t(item.locale)"
class="ms-form-table-input"
:max-length="255"
size="mini"
@input="() => handleFormChange(record, rowIndex, item)"
/>
<a-select
v-else-if="item.inputType === 'select'"
v-model:model-value="record[item.dataIndex as string]"
:options="item.typeOptions || []"
class="ms-form-table-input w-full"
size="mini"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<a-popover
v-else-if="item.inputType === 'tags'"
position="tl"
:disabled="record[item.dataIndex as string].length === 0"
class="ms-params-input-popover"
>
<template #content>
<div class="ms-form-table-popover-title">
{{ t('common.tag') }}
</div>
<div class="ms-form-table-popover-value">
<MsTagsGroup is-string-tag :tag-list="record[item.dataIndex as string]" />
</div>
</template>
<MsTagsInput
v-model:model-value="record[item.dataIndex as string]"
:max-tag-count="1"
input-class="ms-form-table-input"
size="mini"
@change="() => handleFormChange(record, rowIndex, item)"
@clear="() => handleFormChange(record, rowIndex, item)"
/>
</a-popover>
<a-switch
v-else-if="item.inputType === 'switch'"
v-model:model-value="record[item.dataIndex as string]"
size="small"
class="ms-form-table-input-switch"
type="line"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<a-checkbox
v-else-if="item.inputType === 'checkbox'"
v-model:model-value="record[item.dataIndex as string]"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<template v-else-if="item.inputType === 'text'">
{{
typeof item.valueFormat === 'function' ? item.valueFormat(record) : record[item.dataIndex as string] || '-'
}}
</template>
<template v-else-if="item.dataIndex === 'action'">
<div
:key="item.dataIndex"
class="flex flex-row items-center"
:class="{ 'justify-end': item.align === 'right' }"
>
<slot
name="operationPre"
v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }"
></slot>
<MsTableMoreAction
v-if="item.moreAction"
:list="getMoreActionList(item.moreAction, record)"
@select="(e) => handleMoreActionSelect(e, record, item, rowIndex)"
/>
<icon-minus-circle
v-if="dataLength > 1 && rowIndex !== dataLength - 1"
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(record, rowIndex)"
/>
</div>
</template>
</slot>
</template>
</MsBaseTable>
@ -20,26 +123,38 @@
<script setup lang="ts">
import { TableColumnData, TableData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/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 MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/hooks/useTableStore';
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
import { ActionsItem } from '../ms-table-more-action/types';
import { TableOperationColumn } from '@arco-design/web-vue/es/table/interface';
export interface FormTableColumn extends MsTableColumnData {
enable?: boolean; //
required?: boolean; //
inputType?: 'input' | 'select' | 'tags' | 'switch' | 'text' | 'checkbox'; //
valueFormat?: (record: Record<string, any>) => string; // inputTypetext
[key: string]: any; //
}
const props = withDefaults(
defineProps<{
data?: any[];
data?: Record<string, any>[];
columns: FormTableColumn[];
defaultParamDataItem?: Record<string, any>;
scroll?: {
x?: number | string;
y?: number | string;
@ -53,7 +168,6 @@
tableKey?: TableKeyEnum; // key showSettingtrue
disabled?: boolean; //
showSelectorAll?: boolean; //
isTreeTable?: boolean; //
spanMethod?: (data: {
record: TableData;
column: TableColumnData | TableOperationColumn;
@ -69,9 +183,18 @@
}
);
const emit = defineEmits<{
(e: 'change', data: any[]): void; //
(e: 'change', data: Record<string, any>[]): void; //
(e: 'formChange', record: Record<string, any>, columnConfig: FormTableColumn, rowIndex: number): void; //
(
e: 'moreActionSelect',
event: ActionsItem,
record: Record<string, any>,
columnConfig: FormTableColumn,
rowIndex: number
): void;
}>();
const { t } = useI18n();
const tableStore = useTableStore();
async function initColumns() {
@ -112,6 +235,8 @@
emit('change', propsRes.value.data);
};
const dataLength = computed(() => propsRes.value.data.length);
watch(
() => selectedKeys.value,
(arr) => {
@ -128,14 +253,83 @@
watch(
() => props.data,
(val) => {
propsRes.value.data = val;
(arr) => {
propsRes.value.data = arr as any[];
},
{
immediate: true,
}
);
function emitChange(from: string, isInit?: boolean) {
if (!isInit) {
emit('change', propsRes.value.data);
}
}
/**
* 当表格输入框变化时给参数表格添加一行数据行
* @param val 输入值
* @param key 当前列的 key
* @param isForce 是否强制添加
*/
function addTableLine(rowIndex: number, addLineDisabled?: boolean, isInit?: boolean) {
if (addLineDisabled) {
return;
}
if (rowIndex === props.data.length - 1) {
//
const id = new Date().getTime().toString();
propsRes.value.data.push({
id,
...cloneDeep(props.defaultParamDataItem), //
enable: true, //
} as any);
emitChange('addTableLine', isInit);
}
}
function handleFormChange(record: Record<string, any>, rowIndex: number, columnConfig: FormTableColumn) {
emit('formChange', record, columnConfig, rowIndex);
addTableLine(rowIndex, columnConfig.addLineDisabled);
}
function toggleRequired(record: Record<string, any>, rowIndex: number, columnConfig: FormTableColumn) {
record.required = !record.required;
emit('formChange', record, columnConfig, rowIndex);
addTableLine(rowIndex, columnConfig.addLineDisabled);
}
/**
* 获取更多操作按钮列表
* @param actions 按钮列表
* @param record 当前行数据
*/
function getMoreActionList(actions: ActionsItem[], record: Record<string, any>) {
if (props.columns.findIndex((e) => e.dataIndex === 'expression') !== -1) {
// expressionexpression
if (record.expression === '' || record.expression === undefined || record.expression === null) {
return actions.map((e) => ({ ...e, disabled: true }));
}
return actions;
}
return actions;
}
function handleMoreActionSelect(
event: ActionsItem,
record: Record<string, any>,
columnConfig: FormTableColumn,
rowIndex: number
) {
emit('moreActionSelect', event, record, columnConfig, rowIndex);
}
function deleteParam(record: Record<string, any>, rowIndex: number) {
propsRes.value.data.splice(rowIndex, 1);
emitChange('deleteParam');
}
await initColumns();
</script>
@ -147,6 +341,11 @@
:deep(.arco-table .arco-table-cell) {
padding: 8px 2px;
}
.ms-form-table-no-left-action {
:deep(.arco-table .arco-table-cell) {
padding: 8px 16px;
}
}
:deep(.arco-table-cell-align-left) {
padding: 8px;
}
@ -155,8 +354,18 @@
padding: 8px;
}
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
:deep(.ms-table-row-disabled) {
td {
background-color: white !important;
}
* {
color: var(--color-text-4) !important;
}
}
:deep(.ms-form-table-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
@apply bg-transparent;
border-color: transparent !important;
.arco-input::placeholder {
@apply invisible;
@ -172,8 +381,8 @@
}
}
}
:deep(.param-input-number) {
@apply pr-0;
:deep(.ms-form-table-input-number) {
@apply bg-transparent pr-0;
.arco-input {
@apply text-right;
}
@ -193,4 +402,20 @@
:deep(.arco-table-expand-btn) {
background: transparent;
}
.ms-form-table-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.ms-form-table-popover-value {
min-width: 100px;
max-width: 280px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1,4 @@
export default {
'msFormTable.paramRequired': '必填',
'msFormTable.paramNotRequired': '非必填',
};

View File

@ -147,6 +147,7 @@
placement="top"
content-class="max-w-[400px]"
:content="String(record[item.dataIndex as string])"
:disabled="record[item.dataIndex as string] === '' || record[item.dataIndex as string] === undefined || record[item.dataIndex as string] === null"
>
<div class="one-line-text">
<slot :name="item.slotName" v-bind="{ record, rowIndex, column, columnConfig: item }">

View File

@ -1,10 +1,10 @@
<template>
<a-tooltip :content="tagsTooltip">
<div class="flex max-w-[440px] flex-row">
<MsTag v-for="tag of showTagList" :key="tag.id" :width="getTagWidth(tag)" v-bind="attrs">
<MsTag v-for="tag of showTagList" :key="tag.id" :width="getTagWidth(tag)" :size="props.size" v-bind="attrs">
{{ props.isStringTag ? tag : tag[props.nameKey] }}
</MsTag>
<MsTag v-if="props.tagList.length > props.showNum" :width="numberTagWidth" v-bind="attrs">
<MsTag v-if="props.tagList.length > props.showNum" :width="numberTagWidth" :size="props.size" v-bind="attrs">
+{{ props.tagList.length - props.showNum }}</MsTag
>
</div>
@ -14,7 +14,7 @@
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import MsTag from './ms-tag.vue';
import MsTag, { Size } from './ms-tag.vue';
const props = withDefaults(
defineProps<{
@ -22,10 +22,12 @@
showNum?: number;
nameKey?: string;
isStringTag?: boolean; //
size?: Size;
}>(),
{
showNum: 2,
nameKey: 'name',
size: 'medium',
}
);

View File

@ -1,3 +1,4 @@
// @ts-ignore @typescript-eslint/no-duplicate-enum-values
export enum StatusType {
UN_REVIEWED = 'icon-icon_block_filled', // 未评审
UNDER_REVIEWED = 'icon-icon_testing', // 评审中

View File

@ -43,7 +43,7 @@ export enum TableKeyEnum {
CASE_MANAGEMENT_REVIEW = 'caseManagementReview',
CASE_MANAGEMENT_REVIEW_CASE = 'caseManagementReviewCase',
CASE_MANAGEMENT_TAB_DEFECT = 'caseManagementTabDefect',
CASE_MANAGEMENT_TAB_DEFECT_TEST_PLAN = 'caseManagementTabTestPlan',
// CASE_MANAGEMENT_TAB_DEFECT_TEST_PLAN = 'caseManagementTabTestPlan',
CASE_MANAGEMENT_TAB_DEPENDENCY_PRE_CASE = 'caseManagementTabPreDependency',
CASE_MANAGEMENT_TAB_DEPENDENCY_POST_CASE = 'caseManagementTabPostDependency',
CASE_MANAGEMENT_TAB_REVIEW = 'caseManagementTabCaseReview',

View File

@ -14,7 +14,7 @@ export enum ExecutionMethods {
SCHEDULE = 'SCHEDULE', // 定时任务
MANUAL = 'MANUAL', // 手动执行
API = 'API', // 接口调用
BATCH = 'API', // 批量执行
// BATCH = 'API', // 批量执行
}
export enum ExecutionMethodsLabel {

View File

@ -126,4 +126,8 @@ export default {
'common.module': 'Module',
'common.yes': 'Yes',
'common.no': 'No',
'common.creator': 'Creator',
'common.followSuccess': 'Followed',
'common.unFollowSuccess': 'Unfollow successfully',
'common.share': 'Share',
};

View File

@ -129,4 +129,8 @@ export default {
'common.module': '模块',
'common.yes': '是',
'common.no': '否',
'common.creator': '创建人',
'common.followSuccess': '关注成功',
'common.unFollowSuccess': '取消关注成功',
'common.share': '分享',
};

View File

@ -145,7 +145,7 @@ export interface JsonSchema {
export interface ExecuteJsonBody {
enableJsonSchema?: boolean;
enableTransition?: boolean;
jsonSchema?: JsonSchema;
jsonSchema?: JsonSchema[];
jsonValue: string;
}
// 执行请求配置

View File

@ -17,8 +17,8 @@ function setupPageGuard(router: Router) {
// 取消上个路由未完成的请求不包含设置了ignoreCancelToken的请求
axiosCanceler.removeAllPending();
const appStore = useAppStore();
const urlOrgId = to.query.organizationId;
const urlProjectId = to.query.projectId;
const urlOrgId = to.query.orgId;
const urlProjectId = to.query.pId;
// 如果访问页面的时候携带了项目 ID 或组织 ID则将页面上的组织 ID和项目 ID设置为当前选中的组织和项目
if (urlOrgId) {
appStore.setCurrentOrgId(urlOrgId as string);
@ -31,7 +31,7 @@ function setupPageGuard(router: Router) {
if (urlOrgId === undefined) {
to.query = {
...to.query,
organizationId: appStore.currentOrgId,
orgId: appStore.currentOrgId,
};
next(to);
return;
@ -41,8 +41,8 @@ function setupPageGuard(router: Router) {
if (urlOrgId === undefined && urlProjectId === undefined) {
to.query = {
...to.query,
organizationId: appStore.currentOrgId,
projectId: appStore.currentProjectId,
orgId: appStore.currentOrgId,
pId: appStore.currentProjectId,
};
next(to);

View File

@ -1,9 +1,9 @@
<template>
<MsTag :self-style="status.style"> {{ status.text }}</MsTag>
<MsTag :self-style="status.style" :size="props.size"> {{ status.text }}</MsTag>
</template>
<script setup lang="ts">
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
@ -11,6 +11,7 @@
const props = defineProps<{
status: RequestDefinitionStatus;
size?: Size;
}>();
const { t } = useI18n();

View File

@ -30,7 +30,7 @@
v-if="showBatchAddParamDrawer"
v-model:model-value="batchParamsCode"
class="flex-1"
theme="MS-text"
theme="vs"
height="100%"
:show-full-screen="false"
:show-theme-change="false"

View File

@ -52,6 +52,8 @@
<style lang="less" scoped>
.param-input:not(.arco-input-focus) {
&:not(:hover) {
@apply bg-transparent;
border-color: transparent;
}
}

View File

@ -72,7 +72,7 @@
<a-input
v-model:model-value="record[columnConfig.dataIndex as string]"
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
class="param-input"
class="ms-form-table-input"
:max-length="255"
size="mini"
@input="() => addTableLine(rowIndex, columnConfig.addLineDisabled)"
@ -99,7 +99,7 @@
<a-select
v-model:model-value="record.paramType"
:options="columnConfig.typeOptions || []"
class="param-input w-full"
class="ms-form-table-input w-full"
size="mini"
@change="(val) => handleTypeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
/>
@ -108,7 +108,7 @@
<a-select
v-model:model-value="record.extractType"
:options="columnConfig.typeOptions || []"
class="param-input w-[110px]"
class="ms-form-table-input w-[110px]"
size="mini"
@change="() => addTableLine(rowIndex)"
/>
@ -117,7 +117,7 @@
<a-select
v-model:model-value="record.variableType"
:options="columnConfig.typeOptions || []"
class="param-input w-[110px]"
class="ms-form-table-input w-[110px]"
size="mini"
@change="() => addTableLine(rowIndex)"
/>
@ -126,7 +126,7 @@
<a-select
v-model:model-value="record.extractScope"
:options="columnConfig.typeOptions || []"
class="param-input w-[180px]"
class="ms-form-table-input w-[180px]"
size="mini"
@change="() => addTableLine(rowIndex)"
/>
@ -151,7 +151,7 @@
</template>
<a-input
v-model:model-value="record.value"
class="param-input"
class="ms-form-table-input"
:placeholder="t('apiTestDebug.commonPlaceholder')"
:max-length="255"
size="mini"
@ -170,7 +170,7 @@
:file-save-as-source-id="props.fileSaveAsSourceId"
:file-save-as-api="props.fileSaveAsApi"
:file-module-options-api="props.fileModuleOptionsApi"
input-class="param-input h-[24px]"
input-class="ms-form-table-input h-[24px]"
input-size="small"
tag-size="small"
@change="(files, file) => handleFileChange(files, record, rowIndex, file)"
@ -190,8 +190,9 @@
v-model:model-value="record.minLength"
:placeholder="t('apiTestDebug.paramMin')"
:min="0"
class="param-input param-input-number"
class="ms-form-table-input ms-form-table-input-number"
size="mini"
model-event="input"
@change="() => addTableLine(rowIndex)"
/>
<div class="mx-[4px]">{{ t('common.to') }}</div>
@ -199,13 +200,14 @@
v-model:model-value="record.maxLength"
:placeholder="t('apiTestDebug.paramMax')"
:min="0"
class="param-input"
class="ms-form-table-input"
size="mini"
model-event="input"
@change="() => addTableLine(rowIndex)"
/>
</div>
</template>
<template #tag="{ record, columnConfig }">
<template #tag="{ record, columnConfig, rowIndex }">
<a-popover
position="tl"
:disabled="record[columnConfig.dataIndex as string].length === 0"
@ -222,8 +224,10 @@
<MsTagsInput
v-model:model-value="record[columnConfig.dataIndex as string]"
:max-tag-count="1"
input-class="param-input"
input-class="ms-form-table-input"
size="mini"
@change="() => addTableLine(rowIndex)"
@clear="() => addTableLine(rowIndex)"
/>
</a-popover>
</template>
@ -240,7 +244,7 @@
<a-switch
v-model:model-value="record.encode"
size="small"
class="param-input-switch"
class="ms-form-table-input-switch"
type="line"
@change="() => addTableLine(rowIndex)"
/>
@ -259,13 +263,13 @@
</template>
<!-- 响应头 -->
<template #header="{ record, columnConfig, rowIndex }">
<a-select v-model="record.header" class="param-input" size="mini" @change="() => addTableLine(rowIndex)">
<a-select v-model="record.header" class="ms-form-table-input" size="mini" @change="() => addTableLine(rowIndex)">
<a-option v-for="item in columnConfig.options" :key="item.value">{{ t(item.label) }}</a-option>
</a-select>
</template>
<!-- 匹配条件 -->
<template #condition="{ record, columnConfig }">
<a-select v-model="record.condition" size="mini" class="param-input">
<a-select v-model="record.condition" size="mini" class="ms-form-table-input">
<a-option v-for="item in columnConfig.options" :key="item.value" :value="item.value">{{
t(item.label)
}}</a-option>
@ -289,12 +293,12 @@
<div>*</div>
</MsButton>
</a-tooltip>
<a-input v-model="record.expectedValue" size="mini" class="param-input" />
<a-input v-model="record.expectedValue" size="mini" class="ms-form-table-input" />
</template>
<template #project="{ record, rowIndex }">
<a-select
v-model:model-value="record.projectId"
class="param-input w-max-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
class="ms-form-table-input w-max-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
:bordered="false"
allow-search
@change="(val) => handleProjectChange(val as string,record.projectId, rowIndex)"
@ -330,7 +334,7 @@
:search-keys="['name']"
size="mini"
allow-search
class="param-input"
class="ms-form-table-input"
:remote-func="initEnvOptions"
:remote-extra-params="{ projectId: record.projectId, keyword: record.environmentInput }"
@change-object="(val) => handleEnvironment(val, record)"
@ -413,7 +417,7 @@
<MsCodeEditor
v-if="showQuickInputParam"
v-model:model-value="quickInputParamValue"
theme="MS-text"
theme="vs"
height="300px"
:show-full-screen="false"
>

View File

@ -150,7 +150,6 @@
emit('renameFinish', form.value.field, props.nodeId);
} else if (props.mode === 'tabRename') {
// tab
Message.success(t('common.updateSuccess'));
emit('renameFinish', form.value.field);
}
if (done) {

View File

@ -69,6 +69,11 @@
dataIndex: 'driver',
showTooltip: true,
},
{
title: 'URL',
dataIndex: 'dbUrl',
showTooltip: true,
},
{
title: 'apiTestDebug.username',
dataIndex: 'username',

View File

@ -249,8 +249,8 @@
v-show="showResponse"
v-model:active-layout="activeLayout"
v-model:active-tab="requestVModel.responseActiveTab"
v-model:response-definition="requestVModel.responseDefinition"
:is-expanded="isExpanded"
:response-definition="requestVModel.responseDefinition"
:hide-layout-switch="props.hideResponseLayoutSwitch"
:request-task-result="requestVModel.response"
:is-edit="props.isDefinition && isHttpProtocol"
@ -376,6 +376,7 @@
activeTab: RequestComposition;
mode?: 'definition' | 'debug';
executeLoading: boolean; // loading
isCopy?: boolean; //
}
export type RequestParam = ExecuteApiRequestFullParams & {
responseDefinition?: ResponseItem[];
@ -616,7 +617,7 @@
*/
function setPluginFormData() {
const tempForm = temporaryPluginFormMap[requestVModel.value.id];
if (tempForm || !requestVModel.value.isNew) {
if (tempForm || !requestVModel.value.isNew || requestVModel.value.isCopy) {
//
const formData = tempForm || requestVModel.value;
if (fApi.value) {
@ -918,13 +919,17 @@
let requestModuleId = '';
let apiDefinitionParams: Record<string, any> = {};
if (props.isDefinition) {
//
requestName = requestVModel.value.name;
requestModuleId = requestVModel.value.moduleId;
apiDefinitionParams = {
tags: requestVModel.value.tags,
description: requestVModel.value.description,
status: requestVModel.value.status,
response: requestVModel.value.responseDefinition,
response: requestVModel.value.responseDefinition?.map((e) => ({
...e,
headers: filterKeyValParams(e.headers, defaultKeyValueParamItem).validParams,
})),
};
} else {
requestName = requestVModel.value.isNew ? saveModalForm.value.name : requestVModel.value.name;

View File

@ -10,7 +10,7 @@
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.defaultFlag" class="response-tab-default-icon"></div>
{{ t(tab.label || '') }}({{ tab.statusCode }})
{{ t(tab.name || tab.label) }}({{ tab.statusCode }})
<MsMoreAction
:list="
tab.defaultFlag
@ -23,12 +23,13 @@
<popConfirm
v-model:visible="tab.showRenamePopConfirm"
mode="tabRename"
:field-config="{ field: t(tab.label || '') }"
:all-names="responseTabs.map((e) => t(tab.label || ''))"
:field-config="{ field: t(tab.label || tab.name) }"
:all-names="responseTabs.map((e) => t(e.label || e.name))"
:popup-offset="20"
@rename-finish="
(val) => {
tab.label = val;
tab.name = val;
emit('change');
}
"
@ -48,7 +49,7 @@
</template>
<template #content>
<div class="font-semibold text-[var(--color-text-1)]">
{{ t('apiTestManagement.confirmDelete', { name: tab.label }) }}
{{ t('apiTestManagement.confirmDelete', { name: tab.label || tab.name }) }}
</div>
</template>
<div class="relative"></div>
@ -72,11 +73,16 @@
size="small"
@change="(val) => changeBodyFormat(val as ResponseBodyFormat)"
>
<a-radio v-for="item of ResponseBodyFormat" :key="item" :value="item">
<a-radio
v-for="item of ResponseBodyFormat"
v-show="item !== ResponseBodyFormat.NONE"
:key="item"
:value="item"
>
{{ ResponseBodyFormat[item].toLowerCase() }}
</a-radio>
</a-radio-group>
<div v-if="activeResponse.body.bodyType === ResponseBodyFormat.JSON" class="ml-auto flex items-center">
<!-- <div v-if="activeResponse.body.bodyType === ResponseBodyFormat.JSON" class="ml-auto flex items-center">
<a-radio-group
v-model:model-value="activeResponse.body.jsonBody.enableJsonSchema"
size="mini"
@ -89,7 +95,7 @@
<a-switch v-model:model-value="activeResponse.body.jsonBody.enableTransition" size="small" type="line" />
{{ t('apiTestManagement.dynamicConversion') }}
</div>
</div>
</div> -->
</div>
<div
v-if="
@ -99,6 +105,11 @@
"
class="h-[calc(100%-35px)]"
>
<!-- <MsJsonSchema
v-if="activeResponse.body.jsonBody.enableJsonSchema"
:data="activeResponse.body.jsonBody.jsonSchema"
:columns="jsonSchemaColumns"
/> -->
<MsCodeEditor
ref="responseEditorRef"
v-model:model-value="currentBodyCode"
@ -137,16 +148,17 @@
v-else-if="activeResponse.responseActiveTab === ResponseComposition.HEADER"
v-model:params="activeResponse.headers"
:columns="columns"
:default-param-item="[
{
key: '',
value: '',
},
]"
:default-param-item="defaultKeyValueParamItem"
:selectable="false"
@change="emit('change')"
@change="handleResponseTableChange"
/>
<a-select
v-else
v-model:model-value="activeResponse.statusCode"
:options="statusCodeOptions"
class="w-[200px]"
@change="handleStatusCodeChange"
/>
<a-select v-else v-model:model-value="activeResponse.statusCode" :options="statusCodeOptions" class="w-[200px]" />
</div>
</template>
@ -157,6 +169,8 @@
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
// import { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
// import MsJsonSchema from '@/components/pure/ms-json-schema/index.vue';
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { MsFileItem } from '@/components/pure/ms-upload/types';
@ -170,7 +184,7 @@
import { ResponseDefinition } from '@/models/apiTest/common';
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
import { defaultResponseItem, statusCodes } from '../../config';
import { defaultKeyValueParamItem, defaultResponseItem, statusCodes } from '../../config';
const props = defineProps<{
responseDefinition: ResponseDefinition[];
@ -193,19 +207,11 @@
});
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),
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
name: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
...defaultProps,
id: new Date().getTime(),
defaultFlag: false,
@ -243,17 +249,23 @@
function handleMoreActionSelect(e: ActionsItem, _tab: ResponseItem) {
switch (e.eventTag) {
case 'setDefault':
responseTabs.value = responseTabs.value.map((tab) => {
tab.defaultFlag = _tab.id === tab.id;
return tab;
});
_tab.defaultFlag = true;
responseTabs.value = [
_tab,
...responseTabs.value.filter((tab) => {
if (tab.id !== _tab.id) {
tab.defaultFlag = false;
}
return tab.id !== _tab.id;
}),
];
break;
case 'rename':
renameValue.value = _tab.label || '';
renameValue.value = _tab.label || _tab.name || '';
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
break;
case 'copy':
addResponseTab({ ..._tab, label: `${_tab.label}-Copy` });
addResponseTab({ ..._tab, label: `${_tab.label || _tab.name}-Copy`, name: `${_tab.label || _tab.name}-Copy` });
break;
case 'delete':
_tab.showPopConfirm = true;
@ -291,6 +303,21 @@
emit('change');
}
// const jsonSchemaColumns: FormTableColumn[] = [
// {
// title: 'apiTestManagement.paramName',
// dataIndex: 'key',
// slotName: 'key',
// inputType: 'input',
// },
// {
// title: 'apiTestManagement.paramVal',
// dataIndex: 'value',
// slotName: 'value',
// inputType: 'input',
// },
// ];
//
const currentBodyCode = computed({
get() {
@ -365,12 +392,20 @@
title: 'apiTestManagement.paramName',
dataIndex: 'key',
slotName: 'key',
inputType: 'input',
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
slotName: 'value',
isNormal: true,
inputType: 'input',
},
{
title: '',
dataIndex: 'operation',
slotName: 'operation',
width: 35,
},
];
@ -378,10 +413,22 @@
label: e.toString(),
value: e,
}));
function handleResponseTableChange(arr: any[]) {
activeResponse.value.headers = [...arr];
emit('change');
}
function handleStatusCodeChange() {
emit('change');
}
</script>
<style lang="less" scoped>
.response-container {
@apply overflow-y-auto;
.ms-scroll-bar();
margin-top: 8px;
height: calc(100% - 88px);
.response-header-pre {

View File

@ -117,14 +117,14 @@
</div>
<a-spin :loading="props.loading" class="h-[calc(100%-35px)] w-full px-[18px] pb-[18px]">
<edit
v-if="props.isEdit && activeResponseType === 'content' && validResponseDefinition"
:response-definition="validResponseDefinition"
v-if="props.isEdit && activeResponseType === 'content' && innerResponseDefinition"
v-model:response-definition="innerResponseDefinition"
:upload-temp-file-api="props.uploadTempFileApi"
@change="handleResponseChange"
/>
<result
v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
v-model:activeTab="activeTab"
v-model:active-tab="innerActiveTab"
:request-result="props.requestTaskResult?.requestResults[0]"
:console="props.requestTaskResult?.console"
/>
@ -133,8 +133,6 @@
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import type { Direction } from '@/components/pure/ms-split-box/index.vue';
import edit, { ResponseItem } from './edit.vue';
@ -164,8 +162,6 @@
}
);
const emit = defineEmits<{
(e: 'update:activeLayout', value: Direction): void;
(e: 'update:activeTab', value: ResponseComposition): void;
(e: 'changeExpand', value: boolean): void;
(e: 'changeLayout', value: Direction): void;
(e: 'change'): void;
@ -173,8 +169,15 @@
const { t } = useI18n();
const innerLayout = useVModel(props, 'activeLayout', emit);
const activeTab = useVModel(props, 'activeTab', emit);
const innerLayout = defineModel<Direction>('activeLayout', {
default: 'vertical',
});
const innerActiveTab = defineModel<ResponseComposition>('activeTab', {
required: true,
});
const innerResponseDefinition = defineModel<ResponseItem[]>('responseDefinition', {
default: [],
});
//
const timingInfo = computed(() => {
if (props.requestTaskResult) {
@ -215,44 +218,56 @@
}
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: '',
};
watchEffect(() => {
// null
let hasInvalid = false;
let validResponseDefinition: ResponseItem[] = [];
if (props.responseDefinition && props.responseDefinition.length > 0) {
validResponseDefinition = props.responseDefinition.map((item, i) => {
// null
if (!item.headers) {
item.headers = [];
hasInvalid = true;
}
if (!item.body.rawBody) {
item.body.rawBody = {
value: '',
};
if (!item.id) {
item.id = new Date().getTime() + i;
hasInvalid = true;
}
}
return item;
});
if (item.body.bodyType === ResponseBodyFormat.NONE) {
item.body.bodyType = ResponseBodyFormat.RAW;
hasInvalid = true;
}
if (!item.body.binaryBody) {
item.body.binaryBody = {
description: '',
file: undefined,
};
hasInvalid = true;
}
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: '',
};
}
hasInvalid = true;
}
return item;
});
}
if (hasInvalid) {
innerResponseDefinition.value = validResponseDefinition;
}
});
function handleResponseChange() {

View File

@ -1,8 +1,17 @@
import { cloneDeep, isEqual } from 'lodash-es';
import { RequestParam } from './requestComposition/index.vue';
import { ExecuteBody } from '@/models/apiTest/common';
import { RequestParamsType } from '@/enums/apiEnum';
import {
defaultBodyParamsItem,
defaultHeaderParamsItem,
defaultKeyValueParamItem,
defaultRequestParamsItem,
} from './config';
export interface ParseResult {
uploadFileIds: string[];
linkFileIds: string[];
@ -133,3 +142,23 @@ export function filterKeyValParams<T>(params: (T & Record<string, any>)[], defau
validParams,
};
}
/**
*
* @param requestVModel
*/
export function getValidRequestTableParams(requestVModel: RequestParam) {
const { formDataBody, wwwFormBody } = requestVModel.body;
return {
formDataBodyTableParams: filterKeyValParams(formDataBody.formValues, defaultBodyParamsItem).validParams,
wwwFormBodyTableParams: filterKeyValParams(wwwFormBody.formValues, defaultBodyParamsItem).validParams,
headers: filterKeyValParams(requestVModel.headers, defaultHeaderParamsItem).validParams,
query: filterKeyValParams(requestVModel.query, defaultRequestParamsItem).validParams,
rest: filterKeyValParams(requestVModel.rest, defaultRequestParamsItem).validParams,
response:
requestVModel.responseDefinition?.map((e) => ({
...e,
headers: filterKeyValParams(e.headers, defaultKeyValueParamItem).validParams,
})) || [],
};
}

View File

@ -284,7 +284,7 @@
const res = await getDebugDetail(apiInfo.id);
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body);
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
addDebugTab({
label: apiInfo.name,

View File

@ -27,7 +27,7 @@
@selected-change="handleTableSelect"
@batch-action="handleTableBatch"
>
<template #methodFilter="{ columnConfig }">
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="methodFilterVisible"
trigger="click"
@ -78,6 +78,7 @@
</template>
<template #method="{ record }">
<a-select
v-if="props.protocol === 'HTTP'"
v-model:model-value="record.method"
class="param-input w-full"
@change="() => handleMethodChange(record)"
@ -89,6 +90,7 @@
<apiMethodName :method="item" is-tag />
</a-option>
</a-select>
<apiMethodName v-else :method="record.method" is-tag />
</template>
<template #status="{ record }">
<a-select
@ -430,7 +432,11 @@
projectId: appStore.currentProjectId,
moduleIds: moduleIds.value,
protocol: props.protocol,
filter: { status: statusFilters.value, method: methodFilters.value },
filter: {
status:
statusFilters.value.length === Object.keys(RequestDefinitionStatus).length ? undefined : statusFilters.value,
method: methodFilters.value.length === Object.keys(RequestMethods).length ? undefined : methodFilters.value,
},
};
setLoadListParams(params);
loadList();

View File

@ -32,7 +32,21 @@
/>
</div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<a-tabs default-active-key="definition" animation lazy-load class="ms-api-tab-nav">
<a-tabs v-model:active-key="definitionActiveKey" animation lazy-load class="ms-api-tab-nav">
<a-tab-pane
v-if="!activeApiTab.isNew"
key="preview"
:title="t('apiTestManagement.preview')"
class="ms-api-tab-pane"
>
<preview
v-if="definitionActiveKey === 'preview'"
:detail="activeApiTab"
:module-tree="props.moduleTree"
:protocols="protocols"
@update-follow="activeApiTab.follow = !activeApiTab.follow"
/>
</a-tab-pane>
<a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-tab-pane">
<MsSplitBox
ref="splitBoxRef"
@ -68,8 +82,8 @@
/>
</template>
<template #second>
<div class="p-[24px]">
<!-- 第一版没有模板 -->
<div class="p-[18px]">
<!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<a-form ref="activeApiTabFormRef" :model="activeApiTab" layout="vertical">
<a-form-item
@ -127,7 +141,8 @@
/>
</a-form-item>
</a-form>
<div class="mb-[8px] flex items-center">
<!-- TODO:第一版先不做依赖 -->
<!-- <div class="mb-[8px] flex items-center">
<div class="text-[var(--color-text-2)]">
{{ t('apiTestManagement.addDependency') }}
</div>
@ -168,7 +183,7 @@
{{ t('apiTestManagement.addPostDependency') }}
</MsButton>
</div>
</div>
</div> -->
</div>
</template>
</MsSplitBox>
@ -183,10 +198,10 @@
</template>
<script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue';
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
// import MsButton from '@/components/pure/ms-button/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
@ -197,7 +212,7 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { getProtocolList, localExecuteApiDebug } from '@/api/modules/api-test/common';
import {
addDefinition,
debugDefinition,
@ -211,7 +226,7 @@
import useAppStore from '@/store/modules/app';
import { filterTree } from '@/utils';
import { ExecuteBody, RequestTaskResult } from '@/models/apiTest/common';
import { ExecuteBody, ProtocolItem, RequestTaskResult } from '@/models/apiTest/common';
import {
ApiDefinitionCreateParams,
ApiDefinitionDetail,
@ -229,10 +244,12 @@
import { defaultResponseItem } from '@/views/api-test/components/config';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
// requestComposition
const requestComposition = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/index.vue')
);
const preview = defineAsyncComponent(() => import('./preview.vue'));
const props = defineProps<{
activeModule: string;
@ -241,12 +258,27 @@
protocol: string;
}>();
const emit = defineEmits(['addDone']);
const definitionActiveKey = ref('definition');
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const appStore = useAppStore();
const { t } = useI18n();
const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() {
try {
protocols.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initProtocolList();
});
const apiTabs = ref<RequestParam[]>([
{
id: 'all',
@ -401,7 +433,7 @@
isNew: !defaultProps?.id, // tabidid
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
}
const apiTableRef = ref<InstanceType<typeof apiTable>>();
@ -413,8 +445,10 @@
}
const loading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail, isCopy = false) {
const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id);
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false) {
const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
);
if (isLoadedTabIndex > -1 && !isCopy) {
// tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
@ -422,8 +456,13 @@
}
try {
loading.value = true;
const res = await getDefinitionDetail(apiInfo.id);
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
const name = isCopy ? `${res.name}-copy` : res.name;
definitionActiveKey.value = isCopy ? 'definition' : 'preview';
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
addApiTab({
label: name,
...res.request,
@ -434,6 +473,8 @@
name, // requestnamenull
isNew: isCopy,
unSaved: isCopy,
isCopy,
...parseRequestBodyResult,
});
nextTick(() => {
// loading
@ -465,25 +506,25 @@
const showAddDependencyDrawer = ref(false);
const addDependencyMode = ref<'pre' | 'post'>('pre');
function handleDddDependency(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'pre':
addDependencyMode.value = 'pre';
showAddDependencyDrawer.value = true;
break;
case 'post':
addDependencyMode.value = 'post';
showAddDependencyDrawer.value = true;
break;
default:
break;
}
}
// function handleDddDependency(value: string | number | Record<string, any> | undefined) {
// switch (value) {
// case 'pre':
// addDependencyMode.value = 'pre';
// showAddDependencyDrawer.value = true;
// break;
// case 'post':
// addDependencyMode.value = 'post';
// showAddDependencyDrawer.value = true;
// break;
// default:
// break;
// }
// }
function clearAllDependency() {
activeApiTab.value.preDependency = [];
activeApiTab.value.postDependency = [];
}
// function clearAllDependency() {
// activeApiTab.value.preDependency = [];
// activeApiTab.value.postDependency = [];
// }
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const activeApiTabFormRef = ref<FormInstance>();

View File

@ -0,0 +1,862 @@
<template>
<a-spin :loading="pluginLoading" class="h-full w-full overflow-hidden">
<div class="px-[18px] pt-[16px]">
<MsDetailCard
:title="`【${preivewDetail.num}】${preivewDetail.name}`"
:description="description"
:simple-show-count="4"
>
<template #titleAppend>
<apiStatus :status="preivewDetail.status" size="small" />
</template>
<template #titleRight>
<a-button
type="outline"
:loading="followLoading"
size="mini"
class="arco-btn-outline--secondary mr-[4px] !bg-transparent"
@click="toggleFollowReview"
>
<div class="flex items-center gap-[4px]">
<MsIcon
:type="preivewDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${preivewDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
:size="14"
/>
{{ t(preivewDetail.follow ? 'common.forked' : 'common.fork') }}
</div>
</a-button>
<a-button type="outline" size="mini" class="arco-btn-outline--secondary !bg-transparent" @click="share">
<div class="flex items-center gap-[4px]">
<MsIcon type="icon-icon_share1" class="text-[var(--color-text-4)]" :size="14" />
{{ t('common.share') }}
</div>
</a-button>
</template>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
</MsDetailCard>
</div>
<div class="h-[calc(100%-124px)]">
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="overflow-y-auto px-[18px] py-[16px]">
<a-collapse v-model:active-key="activeDetailKey" :bordered="false">
<a-collapse-item key="request">
<template #header>
<div class="flex items-center gap-[4px]">
<div v-if="activeDetailKey.includes('request')" class="down-icon">
<icon-down :size="10" class="block" />
</div>
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]">
<icon-right :size="10" class="block" />
</div>
<div class="font-medium">{{ t('apiTestManagement.requestParams') }}</div>
</div>
</template>
<div class="detail-collapse-item">
<template v-if="props.detail.protocol === 'HTTP'">
<div v-if="preivewDetail.headers.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">{{ t('apiTestManagement.requestHeader') }}</div>
<a-radio-group v-model:model-value="headerShowType" type="button" size="mini">
<a-radio value="table">Table</a-radio>
<a-radio value="raw">Raw</a-radio>
</a-radio-group>
</div>
<MsFormTable
v-show="headerShowType === 'table'"
:columns="headerColumns"
:data="preivewDetail.headers || []"
:selectable="false"
/>
<MsCodeEditor
v-show="headerShowType === 'raw'"
:model-value="headerRawCode"
class="flex-1"
theme="MS-text"
height="200px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(headerRawCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
<div v-if="preivewDetail.query.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">Query</div>
<a-radio-group v-model:model-value="queryShowType" type="button" size="mini">
<a-radio value="table">Table</a-radio>
<a-radio value="raw">Raw</a-radio>
</a-radio-group>
</div>
<MsFormTable
v-show="queryShowType === 'table'"
:columns="queryRestColumns"
:data="preivewDetail.query || []"
:selectable="false"
/>
<MsCodeEditor
v-show="queryShowType === 'raw'"
:model-value="queryRawCode"
class="flex-1"
theme="MS-text"
height="200px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(queryRawCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
<div v-if="preivewDetail.rest.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">Rest</div>
<a-radio-group v-model:model-value="restShowType" type="button" size="mini">
<a-radio value="table">Table</a-radio>
<a-radio value="raw">Raw</a-radio>
</a-radio-group>
</div>
<MsFormTable
v-show="restShowType === 'table'"
:columns="queryRestColumns"
:data="preivewDetail.rest || []"
:selectable="false"
/>
<MsCodeEditor
v-show="restShowType === 'raw'"
:model-value="restRawCode"
class="flex-1"
theme="MS-text"
height="200px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(restRawCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
<div class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ `${t('apiTestManagement.requestBody')}-${preivewDetail.body.bodyType}` }}
</div>
<!-- <a-radio-group
v-if="preivewDetail.body.bodyType !== RequestBodyFormat.NONE"
v-model:model-value="bodyShowType"
type="button"
size="mini"
>
<a-radio value="table">Table</a-radio>
<a-radio value="code">Code</a-radio>
</a-radio-group> -->
</div>
<div
v-if="preivewDetail.body.bodyType === RequestBodyFormat.NONE"
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
>
{{ t('apiTestDebug.noneBody') }}
</div>
<MsFormTable
v-else-if="
preivewDetail.body.bodyType === RequestBodyFormat.FORM_DATA ||
preivewDetail.body.bodyType === RequestBodyFormat.WWW_FORM
"
:columns="bodyColumns"
:data="bodyTableData"
:selectable="false"
/>
<MsCodeEditor
v-else-if="
[RequestBodyFormat.JSON, RequestBodyFormat.RAW, RequestBodyFormat.XML].includes(
preivewDetail.body.bodyType
)
"
:model-value="bodyCode"
class="flex-1"
theme="vs"
height="200px"
:language="bodyCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(bodyCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
</template>
<div v-else class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">{{ t('apiTestManagement.requestData') }}</div>
<a-radio-group v-model:model-value="pluginShowType" type="button" size="mini">
<a-radio value="table">Table</a-radio>
<a-radio value="raw">Raw</a-radio>
</a-radio-group>
</div>
<MsFormTable
v-show="pluginShowType === 'table'"
:columns="pluginTableColumns"
:data="pluginTableData"
:selectable="false"
/>
<MsCodeEditor
v-show="pluginShowType === 'raw'"
:model-value="pluginRawCode"
class="flex-1"
theme="MS-text"
height="400px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(pluginRawCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
</div>
</a-collapse-item>
<a-collapse-item
v-if="
preivewDetail.responseDefinition &&
preivewDetail.responseDefinition.length > 0 &&
props.detail.protocol === 'HTTP'
"
key="response"
>
<template #header>
<div class="flex items-center gap-[4px]">
<div v-if="activeDetailKey.includes('response')" class="down-icon">
<icon-down :size="10" class="block" />
</div>
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]">
<icon-right :size="10" class="block" />
</div>
<div class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
</div>
</template>
<MsEditableTab
v-model:active-tab="activeResponse"
:tabs="preivewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
hide-more-action
readonly
class="my-[8px]"
>
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.defaultFlag" class="response-tab-default-icon"></div>
{{ t(tab.label || tab.name) }}({{ tab.statusCode }})
</div>
</template>
</MsEditableTab>
<div class="detail-item !pt-0">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ `${t('apiTestDebug.responseBody')}-${activeResponse?.body.bodyType}` }}
</div>
</div>
<MsCodeEditor
v-if="activeResponse?.body.bodyType !== ResponseBodyFormat.BINARY"
:model-value="responseCode"
class="flex-1"
theme="vs"
height="200px"
:language="responseCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(responseCode || '')"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</div>
<div v-if="activeResponse?.headers && activeResponse?.headers.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ t('apiTestDebug.responseHeader') }}
</div>
</div>
<MsFormTable
:columns="responseHeaderColumns"
:data="activeResponse?.headers || []"
:selectable="false"
/>
</div>
</a-collapse-item>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]"> </a-tab-pane>
<a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
</a-tab-pane>
<a-tab-pane key="changeHistory" :title="t('apiTestManagement.changeHistory')" class="px-[18px] py-[16px]">
</a-tab-pane>
</a-tabs>
</div>
</a-spin>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.vue';
import { getPluginScript } from '@/api/modules/api-test/common';
import { toggleFollowDefinition } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { findNodeByKey } from '@/utils';
import { PluginConfig, ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import { RequestBodyFormat, RequestMethods, RequestParamsType, ResponseBodyFormat } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { getValidRequestTableParams } from '@/views/api-test/components/utils';
const props = defineProps<{
detail: RequestParam;
moduleTree: ModuleTreeNode[];
protocols: ProtocolItem[];
}>();
const emit = defineEmits(['updateFollow']);
const { t } = useI18n();
const { copy, isSupported } = useClipboard();
const preivewDetail = ref<RequestParam>(cloneDeep(props.detail));
const activeResponse = ref<TabItem & ResponseItem>();
const pluginLoading = ref(false);
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); //
const pluginShowType = ref('table');
const pluginTableColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
},
];
const pluginTableData = computed(() => {
if (pluginScriptMap.value[preivewDetail.value.protocol]) {
return (
pluginScriptMap.value[preivewDetail.value.protocol].apiDefinitionFields?.map((e) => ({
key: e,
value: preivewDetail.value[e],
})) || []
);
}
return [];
});
const pluginRawCode = computed(() => {
if (pluginScriptMap.value[preivewDetail.value.protocol]) {
return (
pluginScriptMap.value[preivewDetail.value.protocol].apiDefinitionFields
?.map((e) => `${e}:${preivewDetail.value[e]}`)
.join('\n') || ''
);
}
return '';
});
const pluginError = ref(false);
async function initPluginScript(protocol: string) {
const pluginId = props.protocols.find((e) => e.protocol === protocol)?.pluginId;
if (!pluginId) {
Message.warning(t('apiTestDebug.noPluginTip'));
pluginError.value = true;
return;
}
pluginError.value = false;
if (pluginScriptMap.value[protocol] !== undefined) {
//
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(pluginId);
pluginScriptMap.value[protocol] = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
watchEffect(() => {
preivewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
const tableParam = getValidRequestTableParams(preivewDetail.value); // props.detail
preivewDetail.value = {
...preivewDetail.value,
body: {
...preivewDetail.value.body,
formDataBody: {
formValues: tableParam.formDataBodyTableParams,
},
wwwFormBody: {
formValues: tableParam.wwwFormBodyTableParams,
},
},
headers: tableParam.headers,
rest: tableParam.rest,
query: tableParam.query,
responseDefinition: tableParam.response,
};
[activeResponse.value] = tableParam.response;
if (preivewDetail.value.protocol !== 'HTTP') {
//
initPluginScript(preivewDetail.value.protocol);
}
});
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: preivewDetail.value.method,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: preivewDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: preivewDetail.value.tags,
},
{
key: 'description',
locale: 'common.desc',
value: preivewDetail.value.description,
width: '100%',
},
{
key: 'belongModule',
locale: 'apiTestManagement.belongModule',
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, preivewDetail.value.moduleId, 'id')?.path,
},
{
key: 'creator',
locale: 'common.creator',
value: preivewDetail.value.createUserName,
},
{
key: 'createTime',
locale: 'apiTestManagement.createTime',
value: dayjs(preivewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
key: 'updateTime',
locale: 'apiTestManagement.updateTime',
value: dayjs(preivewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
]);
const followLoading = ref(false);
async function toggleFollowReview() {
try {
followLoading.value = true;
await toggleFollowDefinition(preivewDetail.value.id);
Message.success(preivewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
emit('updateFollow');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
followLoading.value = false;
}
}
function share() {
if (isSupported) {
copy(`${window.location.href}&dId=${preivewDetail.value.id}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
const activeKey = ref('detail');
const activeDetailKey = ref(['request', 'response']);
async function copyScript(val: string) {
if (isSupported) {
await copy(val);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
/**
* 请求头
*/
const headerColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
},
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
];
const headerShowType = ref('table');
const headerRawCode = computed(() => {
return preivewDetail.value.headers?.map((item) => `${item.key}:${item.value}`).join('\n');
});
/**
* Query & Rest
*/
const queryRestColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'paramType',
inputType: 'text',
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
},
{
title: 'apiTestManagement.required',
dataIndex: 'required',
slotName: 'required',
inputType: 'text',
valueFormat: (record) => {
return record.required ? t('common.yes') : t('common.no');
},
},
{
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
inputType: 'text',
valueFormat: (record) => {
return [null, undefined].includes(record.minLength) && [null, undefined].includes(record.maxLength)
? '-'
: `${record.minLength} ${t('common.to')} ${record.maxLength}`;
},
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
inputType: 'text',
valueFormat: (record) => {
return record.encode ? t('common.yes') : t('common.no');
},
},
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
];
const queryShowType = ref('table');
const queryRawCode = computed(() => {
return preivewDetail.value.query?.map((item) => `${item.key}:${item.value}`).join('\n');
});
const restShowType = ref('table');
const restRawCode = computed(() => {
return preivewDetail.value.rest?.map((item) => `${item.key}:${item.value}`).join('\n');
});
/**
* 请求体
*/
const bodyColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
},
{
title: 'apiTestManagement.paramsType',
dataIndex: 'paramType',
inputType: 'text',
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
showTooltip: true,
},
{
title: 'apiTestManagement.required',
dataIndex: 'required',
slotName: 'required',
inputType: 'text',
valueFormat: (record) => {
return record.required ? t('common.yes') : t('common.no');
},
},
{
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
inputType: 'text',
valueFormat: (record) => {
return [null, undefined].includes(record.minLength) && [null, undefined].includes(record.maxLength)
? '-'
: `${record.minLength} ${t('common.to')} ${record.maxLength}`;
},
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
inputType: 'text',
valueFormat: (record) => {
return record.encode ? t('common.yes') : t('common.no');
},
},
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
width: 100,
},
];
// const bodyShowType = ref('table');
const bodyTableData = computed(() => {
switch (preivewDetail.value.body.bodyType) {
case RequestBodyFormat.FORM_DATA:
return (preivewDetail.value.body.formDataBody?.formValues || []).map((e) => ({
...e,
value: e.paramType === RequestParamsType.FILE ? e.files?.map((file) => file.fileName).join('\n') : e.value,
}));
case RequestBodyFormat.WWW_FORM:
return preivewDetail.value.body.wwwFormBody?.formValues || [];
default:
return [];
}
});
const bodyCode = computed(() => {
switch (preivewDetail.value.body.bodyType) {
case RequestBodyFormat.FORM_DATA:
return preivewDetail.value.body.formDataBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.WWW_FORM:
return preivewDetail.value.body.wwwFormBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.RAW:
return preivewDetail.value.body.rawBody?.value;
case RequestBodyFormat.JSON:
return preivewDetail.value.body.jsonBody?.jsonValue;
case RequestBodyFormat.XML:
return preivewDetail.value.body.xmlBody?.value;
default:
return '';
}
});
const bodyCodeLanguage = computed(() => {
if (preivewDetail.value.body.bodyType === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (preivewDetail.value.body.bodyType === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
/**
* 响应内容
*/
const responseCode = computed(() => {
switch (activeResponse.value?.body.bodyType) {
case ResponseBodyFormat.JSON:
return activeResponse.value?.body.jsonBody?.jsonValue;
case ResponseBodyFormat.XML:
return activeResponse.value?.body.xmlBody?.value;
case ResponseBodyFormat.RAW:
return activeResponse.value?.body.rawBody?.value;
default:
return '';
}
});
const responseCodeLanguage = computed(() => {
if (activeResponse.value?.body.bodyType === ResponseBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (activeResponse.value?.body.bodyType === ResponseBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
const responseHeaderColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
},
];
</script>
<style lang="less" scoped>
.down-icon {
padding: 4px;
width: 16px;
height: 16px;
border-radius: 50%;
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-1));
}
.arco-collapse {
@apply h-full overflow-y-auto;
.ms-scroll-bar();
}
.detail-collapse-item {
@apply overflow-y-auto;
margin-bottom: 16px;
.ms-scroll-bar();
}
.detail-item {
padding-top: 16px;
.detail-item-title {
@apply flex items-center;
margin-bottom: 8px;
gap: 16px;
.detail-item-title-text {
@apply font-medium;
color: var(--color-text-1);
}
}
}
:deep(.arco-collapse) {
border-radius: 0;
.arco-collapse-item-icon-hover {
@apply !hidden;
}
.arco-collapse-item-header {
.arco-collapse-item-header-title {
@apply block w-full;
padding: 8px 16px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
}
}
}
.response-tab {
@apply flex items-center;
.response-tab-default-icon {
@apply rounded-full;
margin-right: 4px;
width: 16px;
height: 16px;
background: url('@/assets/svg/icons/default.svg') no-repeat;
background-size: contain;
box-shadow: 0 0 7px 0 rgb(15 0 78 / 9%);
}
}
</style>

View File

@ -66,7 +66,7 @@
const activeTab = ref('api');
const apiRef = ref<InstanceType<typeof api>>();
function newTab(apiInfo?: ModuleTreeNode) {
function newTab(apiInfo?: ModuleTreeNode | string) {
if (apiInfo) {
apiRef.value?.openApiTab(apiInfo);
} else {

View File

@ -54,6 +54,7 @@
<script lang="ts" setup>
import { provide } from 'vue';
import { useRoute } from 'vue-router';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
@ -64,6 +65,8 @@
import { ModuleTreeNode } from '@/models/common';
const route = useRoute();
const activeModule = ref<string>('all');
const folderTree = ref<ModuleTreeNode[]>([]);
const folderTreePathMap = ref<Record<string, any>>({});
@ -110,6 +113,13 @@
managementRef.value?.refreshApiTable();
}
onMounted(() => {
if (route.query.dId) {
// dId tab
managementRef.value?.newTab(route.query.dId as string);
}
});
/** 向子孙组件提供方法和值 */
provide('setActiveApi', setActiveApi);
provide('refreshModuleTree', refreshModuleTree);

View File

@ -118,4 +118,17 @@ export default {
'mockManagement.batchDisEnable': 'Batch disable',
'mockManagement.batchDeleteMockTip': 'Are you sure you want to delete the selected {count} mocks?',
'apiTestManagement.deleteMockTip': 'After deletion, it cannot be restored. Are you sure you want to delete it?',
'apiTestManagement.preview': 'Preview',
'apiTestManagement.shareUrlCopied': 'Sharing link copied to clipboard',
'apiTestManagement.detail': 'Detail',
'apiTestManagement.reference': 'Reference',
'apiTestManagement.dependencies': 'Dependency',
'apiTestManagement.changeHistory': 'Change history',
'apiTestManagement.requestParams': 'Request parameters',
'apiTestManagement.responseContent': 'Response content',
'apiTestManagement.requestHeader': 'Request header',
'apiTestManagement.requestBody': 'Request body',
'apiTestManagement.paramsType': 'Param type',
'apiTestManagement.required': 'Required',
'apiTestManagement.requestData': 'Request data',
};

View File

@ -111,5 +111,18 @@ export default {
'mockManagement.batchEnable': '批量启用',
'mockManagement.batchDisEnable': '批量禁用',
'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个Mock吗',
'apiTestManagement.deleteMockTip': '刪除后將不可恢復,确认刪除嗎?',
'apiTestManagement.deleteMockTip': '删除后不可恢复,确认删除吗?',
'apiTestManagement.preview': '预览',
'apiTestManagement.shareUrlCopied': '分享链接已复制到剪贴板',
'apiTestManagement.detail': '详情',
'apiTestManagement.reference': '引用关系',
'apiTestManagement.dependencies': '依赖关系',
'apiTestManagement.changeHistory': '变更历史',
'apiTestManagement.requestParams': '请求参数',
'apiTestManagement.responseContent': '响应内容',
'apiTestManagement.requestHeader': '请求头',
'apiTestManagement.requestBody': '请求体',
'apiTestManagement.paramsType': '参数类型',
'apiTestManagement.required': '必填',
'apiTestManagement.requestData': '请求数据',
};

View File

@ -86,7 +86,7 @@
Message.success(t('caseManagement.featureCase.editSuccess'));
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
query: { organizationId: route.query.organizationId, projectId: route.query.projectId },
query: { organizationId: route.query.orgId, projectId: route.query.pId },
});
setState(true);
//
@ -120,8 +120,8 @@
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
query: {
organizationId: route.query.organizationId,
projectId: route.query.projectId,
organizationId: route.query.orgId,
projectId: route.query.pId,
},
});
}