feat(接口管理): 接口定义-详情-变更历史&引用关系列表&部分 bug 解决

This commit is contained in:
baiqi 2024-03-12 14:52:41 +08:00 committed by Craftsman
parent 8065a5ac90
commit ad2759e459
40 changed files with 1535 additions and 996 deletions

View File

@ -0,0 +1,26 @@
<svg width="78" height="60" viewBox="0 0 78 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 0.5H74C75.933 0.5 77.5 2.067 77.5 4V56C77.5 57.933 75.933 59.5 74 59.5H4C2.067 59.5 0.5 57.933 0.5 56V4C0.5 2.067 2.067 0.5 4 0.5Z" fill="#F9F9FE" stroke="white"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H74C75.6569 1 77 2.34315 77 4V7H1V4Z" fill="#EDEDF1"/>
<ellipse cx="9.2623" cy="3.41245" rx="1.4625" ry="1.4625" fill="white"/>
<ellipse cx="14.1373" cy="3.41245" rx="1.4625" ry="1.4625" fill="white"/>
<circle cx="4.3873" cy="3.41245" r="1.4625" fill="white"/>
<rect x="2.67129" y="8.67141" width="72.6572" height="48.6571" rx="1.9" fill="white" stroke="#EDEDF1" stroke-width="0.2"/>
<rect x="5" y="10" width="50" height="4" rx="0.5" fill="#EBF1FF"/>
<rect x="65.8877" y="10" width="8" height="4" rx="0.5" fill="#EDEDF1"/>
<path d="M5 15.5C5 15.2239 5.22386 15 5.5 15H72.5C72.7761 15 73 15.2239 73 15.5V54.5C73 54.7761 72.7761 55 72.5 55H5.5C5.22386 55 5 54.7761 5 54.5V15.5Z" fill="#F9F9FE"/>
<rect x="6" y="17" width="31" height="36" rx="1" fill="white"/>
<rect x="37" y="17" width="35" height="36" rx="1" fill="white"/>
<rect x="7.80078" y="19.9126" width="2.925" height="1.4625" rx="0.5" fill="#AEAEB2"/>
<rect x="39" y="20" width="32" height="1" rx="0.5" fill="#E5F9EF"/>
<rect x="7.80078" y="39.8572" width="2.925" height="1.4625" rx="0.5" fill="#AEAEB2"/>
<rect x="7.80078" y="29.6624" width="6.825" height="1.4625" rx="0.5" fill="#D4D4D8"/>
<rect x="31.6875" y="29.6625" width="3.4125" height="1.4625" rx="0.5" fill="#D4D4D8"/>
<rect x="7.80078" y="22.8374" width="27.3" height="1.4625" rx="0.5" fill="#EDEDF1"/>
<rect x="39" y="24" width="32" height="23" rx="0.5" fill="#F9F9FE"/>
<rect x="7.80078" y="42.7822" width="27.3" height="1.4625" rx="0.5" fill="#EDEDF1"/>
<rect x="7.80078" y="32.5874" width="27.3" height="1.4625" rx="0.5" fill="#EDEDF1"/>
<rect x="7.80078" y="25.7625" width="27.3" height="1.4625" rx="0.5" fill="#EDEDF1"/>
<rect x="7.80078" y="45.7073" width="27.3" height="1.4625" rx="0.5" fill="#EDEDF1"/>
<rect x="57" y="10" width="8" height="4" rx="0.5" fill="#A762BF"/>
<path d="M59.232 11.98L59.39 11.424L59.532 11.448L59.418 11.842H59.768C59.808 11.686 59.84 11.524 59.864 11.352L60.01 11.37C59.984 11.538 59.952 11.694 59.916 11.842H60.852V11.98H59.878C59.854 12.06 59.83 12.136 59.802 12.208H60.62V12.332C60.566 12.504 60.458 12.66 60.294 12.8C60.456 12.896 60.654 12.984 60.886 13.064L60.812 13.198C60.556 13.104 60.344 13.002 60.176 12.892C60.012 13.008 59.808 13.112 59.566 13.206L59.49 13.072C59.714 12.99 59.9 12.9 60.05 12.802C59.894 12.682 59.784 12.552 59.718 12.412C59.572 12.728 59.384 12.978 59.156 13.164L59.082 13.032C59.374 12.776 59.588 12.426 59.728 11.98H59.232ZM59.826 12.34C59.894 12.476 60.008 12.602 60.168 12.718C60.316 12.602 60.418 12.476 60.474 12.34H59.826ZM60.346 11.368C60.49 11.478 60.608 11.588 60.704 11.698L60.602 11.8C60.518 11.694 60.4 11.582 60.25 11.462L60.346 11.368ZM61.252 11.398C61.39 11.502 61.506 11.604 61.6 11.708L61.496 11.81C61.414 11.712 61.3 11.606 61.154 11.492L61.252 11.398ZM62.422 13.164C62.312 13.164 62.192 13.162 62.064 13.16C61.934 13.158 61.828 13.146 61.748 13.124C61.668 13.1 61.596 13.05 61.534 12.978C61.506 12.944 61.48 12.928 61.456 12.928C61.408 12.928 61.328 13.018 61.218 13.202L61.11 13.106C61.216 12.938 61.31 12.836 61.392 12.8V12.192H61.108V12.06H61.526V12.808C61.542 12.82 61.558 12.834 61.576 12.854C61.624 12.912 61.676 12.954 61.732 12.98C61.796 13.008 61.886 13.024 62.006 13.028C62.112 13.03 62.244 13.032 62.402 13.032C62.494 13.032 62.59 13.03 62.688 13.028C62.784 13.026 62.858 13.024 62.91 13.02L62.876 13.164H62.422ZM61.712 12.154H62.196C62.208 12.068 62.216 11.97 62.22 11.858H61.78V11.726H62.384C62.446 11.61 62.5 11.486 62.544 11.354L62.68 11.402C62.636 11.524 62.586 11.632 62.53 11.726H62.79V11.858H62.362C62.358 11.97 62.352 12.07 62.34 12.154H62.858V12.29H62.314C62.258 12.532 62.082 12.732 61.786 12.89L61.694 12.774C61.946 12.65 62.104 12.49 62.164 12.29H61.712V12.154ZM61.986 11.37C62.052 11.458 62.114 11.556 62.174 11.662L62.052 11.722C61.992 11.614 61.926 11.516 61.856 11.432L61.986 11.37ZM62.406 12.408C62.572 12.524 62.722 12.65 62.856 12.786L62.756 12.894C62.616 12.746 62.472 12.616 62.32 12.504L62.406 12.408Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -22,6 +22,9 @@ import {
GetModuleTreeUrl,
ImportDefinitionUrl,
MoveModuleUrl,
OperationHistoryUrl,
RecoverOperationHistoryUrl,
SaveOperationHistoryUrl,
SortDefinitionUrl,
SwitchDefinitionScheduleUrl,
ToggleFollowDefinitionUrl,
@ -50,9 +53,12 @@ import {
ApiDefinitionUpdateParams,
CheckScheduleParams,
CreateImportApiDefinitionScheduleParams,
DefinitionHistoryItem,
DefinitionHistoryPageParams,
EnvModule,
ImportApiDefinitionParams,
mockParams,
RecoverDefinitionParams,
UpdateScheduleParams,
} from '@/models/apiTest/management';
import {
@ -209,6 +215,21 @@ export function toggleFollowDefinition(id: string | number) {
return MSR.get({ url: ToggleFollowDefinitionUrl, params: id });
}
// 接口定义-变更历史
export function operationHistory(data: DefinitionHistoryPageParams) {
return MSR.post<CommonList<DefinitionHistoryItem>>({ url: OperationHistoryUrl, data });
}
// 接口定义-保存变更历史为指定版本
export function saveOperationHistory(data: ExecuteRequestParams) {
return MSR.post({ url: SaveOperationHistoryUrl, data });
}
// 接口定义-恢复至指定变更历史
export function recoverOperationHistory(data: RecoverDefinitionParams) {
return MSR.post({ url: RecoverOperationHistoryUrl, data });
}
/**
* Mock
*/

View File

@ -31,6 +31,10 @@ 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'; // 接口定义-关注/取消关注
export const OperationHistoryUrl = '/api/definition/operation-history'; // 接口定义-变更历史
export const SaveOperationHistoryUrl = '/api/definition/operation-history/save'; // 接口定义-另存变更历史为指定版本
export const RecoverOperationHistoryUrl = '/api/definition/operation-history/recover'; // 接口定义-变更历史恢复
/**
* Mock
*/

View File

@ -97,9 +97,9 @@
</div>
<div class="file-list">
<div v-for="file of alreadyDeleteFiles" :key="file.value" class="file-list-item">
<a-tooltip :content="file.name" :mouse-enter-delay="300">
<a-tooltip :content="file.label" :mouse-enter-delay="300">
<MsTag size="small" max-width="100%">
{{ file.name }}
{{ file.label }}
</MsTag>
</a-tooltip>
<a-tooltip :content="t('ms.add.attachment.remove')">
@ -116,9 +116,9 @@
</div>
<div class="file-list">
<div v-for="file of otherFiles" :key="file.value" class="file-list-item">
<a-tooltip :content="file.name" :mouse-enter-delay="300">
<a-tooltip :content="file.label" :mouse-enter-delay="300">
<MsTag size="small" max-width="100%">
{{ file.name }}
{{ file.label }}
</MsTag>
</a-tooltip>
<div v-if="file.local === true" class="flex items-center">
@ -175,7 +175,7 @@
</template>
<script setup lang="ts">
import { Message, TagData } from '@arco-design/web-vue';
import { TagData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
@ -199,7 +199,6 @@
const props = withDefaults(
defineProps<{
mode?: 'button' | 'input';
fileList: MsFileItem[]; // TODO:MsFileItem
multiple?: boolean;
inputClass?: string;
inputSize?: 'small' | 'medium' | 'large' | 'mini';
@ -222,7 +221,6 @@
}
);
const emit = defineEmits<{
(e: 'update:fileList', fileList: MsFileItem[]): void;
(e: 'upload', file: File): void;
(e: 'change', _fileList: MsFileItem[], fileItem?: MsFileItem): void;
(e: 'linkFile'): void;
@ -232,6 +230,7 @@
const { t } = useI18n();
const innerFileList = defineModel<MsFileItem[]>('fileList', {
// TODO:MsFileItem
required: true,
});
const inputFileName = ref('');
@ -244,9 +243,12 @@
});
const buttonDropDownVisible = ref(false);
watchEffect(() => {
console.log('innerFileList', innerFileList.value);
});
onBeforeMount(() => {
//
const defaultFiles = props.fileList.filter((item) => item) || [];
const defaultFiles = innerFileList.value.filter((item) => item) || [];
if (defaultFiles.length > 0) {
if (props.multiple) {
inputFiles.value = defaultFiles.map((item) => ({
@ -298,7 +300,7 @@
watch(
() => innerFileList.value,
(arr) => {
getListFunParams.value.combine.hiddenIds = innerFileList.value
getListFunParams.value.combine.hiddenIds = arr
.filter((item) => !item.local)
.map((item) => item[props.fields.id] || item.uid);
},

View File

@ -148,8 +148,8 @@
path: route.path,
query: {
...route.query,
organizationId: appStore.currentOrgId,
projectId: appStore.currentProjectId,
orgId: appStore.currentOrgId,
pId: appStore.currentProjectId,
},
});
} catch (error) {

View File

@ -295,8 +295,8 @@
//
function handleNameClick(item: MessageHistoryItem) {
const routeQuery: Record<string, any> = {
organizationId: item.organizationId,
projectId: item.projectId,
orgId: item.organizationId,
pId: item.projectId,
id: item.resourceId,
};
if (item.organizationId === 'SYSTEM') {
@ -340,16 +340,15 @@
.right-align {
float: right;
}
:deep(.arco-list) {
display: flex;
overflow-y: auto;
width: 100%;
font-size: 14px;
border-radius: var(--border-radius-medium);
color: var(--color-text-1);
flex-direction: column;
box-sizing: border-box;
width: 100%;
overflow-y: auto;
color: var(--color-text-1);
font-size: 14px;
line-height: 1.8715;
border-radius: var(--border-radius-medium);
}
</style>

View File

@ -301,7 +301,6 @@
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

@ -54,6 +54,10 @@
color: rgb(var(--danger-2)) !important;
cursor: not-allowed;
}
.ms-button--text--disabled {
color: rgb(var(--primary-3)) !important;
cursor: not-allowed;
}
.ms-button-text {
@apply p-0;

View File

@ -157,13 +157,10 @@
import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/locale/useLocale';
import useAppStore from '@/store/modules/app';
import useLicenseStore from '@/store/modules/setting/license';
import useUserStore from '@/store/modules/user';
import { IconInfoCircle, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
const licenseStore = useLicenseStore();
const props = defineProps<{
isPreview?: boolean;
logo?: string;
@ -212,8 +209,8 @@
path: route.path,
query: {
...route.query,
organizationId: appStore.currentOrgId,
projectId: appStore.currentProjectId,
orgId: appStore.currentOrgId,
pId: appStore.currentProjectId,
},
});
}

View File

@ -5,3 +5,83 @@ export const dropPositionMap: Record<string, any> = {
'0': 'APPEND',
'1': 'AFTER',
};
// 操作类型
export const operationTypeOptions = [
{
label: 'system.log.operateType.all',
value: '',
},
{
label: 'system.log.operateType.add',
value: 'ADD',
},
{
label: 'system.log.operateType.delete',
value: 'DELETE',
},
{
label: 'system.log.operateType.update',
value: 'UPDATE',
},
{
label: 'system.log.operateType.debug',
value: 'DEBUG',
},
{
label: 'system.log.operateType.review',
value: 'REVIEW',
},
{
label: 'system.log.operateType.copy',
value: 'COPY',
},
{
label: 'system.log.operateType.execute',
value: 'EXECUTE',
},
{
label: 'system.log.operateType.share',
value: 'SHARE',
},
{
label: 'system.log.operateType.restore',
value: 'RESTORE',
},
{
label: 'system.log.operateType.import',
value: 'IMPORT',
},
{
label: 'system.log.operateType.export',
value: 'EXPORT',
},
{
label: 'system.log.operateType.login',
value: 'LOGIN',
},
{
label: 'system.log.operateType.select',
value: 'SELECT',
},
{
label: 'system.log.operateType.recover',
value: 'RECOVER',
},
{
label: 'system.log.operateType.logout',
value: 'LOGOUT',
},
{
label: 'system.log.operateType.associate',
value: 'ASSOCIATE',
},
{
label: 'system.log.operateType.disassociate',
value: 'DISASSOCIATE',
},
{
label: 'system.log.operateType.archived',
value: 'ARCHIVED',
},
];

View File

@ -130,4 +130,5 @@ export default {
'common.followSuccess': 'Followed',
'common.unFollowSuccess': 'Unfollow successfully',
'common.share': 'Share',
'common.notRemind': `Don't remind again`,
};

View File

@ -133,4 +133,5 @@ export default {
'common.followSuccess': '关注成功',
'common.unFollowSuccess': '取消关注成功',
'common.share': '分享',
'common.notRemind': '不再提醒',
};

View File

@ -230,3 +230,30 @@ export interface CreateImportApiDefinitionScheduleParams extends ImportApiDefini
value: string; // cron 表达式
config?: string;
}
// 定义-变更历史列表项
export interface DefinitionHistoryItem {
id: number;
projectId: string;
createTime: number;
createUser: string;
sourceId: string;
type: string;
module: string;
refId: number;
createUserName: string;
versionName: string;
}
// 变更历史列表查询参数
export interface DefinitionHistoryPageParams extends TableQueryParams {
projectId: string;
sourceId: string;
createUser: string;
types: string[];
modules: string[];
}
// 定义-恢复历史版本参数
export interface RecoverDefinitionParams {
id: string | number;
sourceId: string | number;
versionId?: string;
}

View File

@ -138,12 +138,12 @@ const useUserStore = defineStore('user', {
const appStore = useAppStore();
setToken(res.sessionId, res.csrfToken);
this.setInfo(res);
const { organizationId, projectId } = getHashParameters();
const { orgId, pId } = getHashParameters();
// 如果访问页面的时候携带了组织 ID和项目 ID则不设置
if (!organizationId || forceSet) {
if (!orgId || forceSet) {
appStore.setCurrentOrgId(res.lastOrganizationId || '');
}
if (!projectId || forceSet) {
if (!pId || forceSet) {
appStore.setCurrentProjectId(res.lastProjectId || '');
}
return true;

View File

@ -174,6 +174,7 @@
input-size="small"
tag-size="small"
@change="(files, file) => handleFileChange(files, record, rowIndex, file)"
@delete-file="() => emitChange('deleteFile')"
/>
<MsParamsInput
v-else

View File

@ -322,7 +322,7 @@
...files,
...resultArr,
currentTableParams.value[currentTableParams.value.length - 1],
];
].filter(Boolean);
}
emit('change');
}

View File

@ -250,6 +250,9 @@
v-model:active-layout="activeLayout"
v-model:active-tab="requestVModel.responseActiveTab"
v-model:response-definition="requestVModel.responseDefinition"
:is-http-protocol="isHttpProtocol"
:is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url"
:is-expanded="isExpanded"
:hide-layout-switch="props.hideResponseLayoutSwitch"
:request-task-result="requestVModel.response"
@ -259,6 +262,7 @@
@change-expand="changeExpand"
@change-layout="handleActiveLayoutChange"
@change="handleActiveDebugChange"
@execute="execute"
/>
</template>
</MsSplitBox>
@ -1049,6 +1053,7 @@
...props.otherParams,
});
requestVModel.value.id = res.id;
requestVModel.value.num = res.num;
requestVModel.value.isNew = false;
Message.success(t('common.saveSuccess'));
requestVModel.value.unSaved = false;

View File

@ -127,6 +127,10 @@
v-model:active-tab="innerActiveTab"
:request-result="props.requestTaskResult?.requestResults[0]"
:console="props.requestTaskResult?.console"
:is-http-protocol="props.isHttpProtocol"
:is-priority-local-exec="props.isPriorityLocalExec"
:request-url="props.requestUrl"
@execute="emit('execute', props.isPriorityLocalExec ? 'localExec' : 'serverExec')"
/>
</a-spin>
</div>
@ -147,8 +151,11 @@
const props = withDefaults(
defineProps<{
activeTab: ResponseComposition;
activeLayout?: Direction;
isExpanded: boolean;
isPriorityLocalExec: boolean;
requestUrl: string;
isHttpProtocol: boolean;
activeLayout?: Direction;
responseDefinition?: ResponseItem[];
requestTaskResult?: RequestTaskResult;
hideLayoutSwitch?: boolean; //
@ -165,6 +172,7 @@
(e: 'changeExpand', value: boolean): void;
(e: 'changeLayout', value: Direction): void;
(e: 'change'): void;
(e: 'execute', executeType: 'localExec' | 'serverExec'): void;
}>();
const { t } = useI18n();

View File

@ -1,67 +1,90 @@
<template>
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]">
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
</a-tabs>
<div class="response-container">
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
show-language-change
show-charset-change
read-only
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<MsCodeEditor
v-else-if="activeTab === ResponseComposition.CONSOLE"
:model-value="props.console?.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
</MsCodeEditor>
<div
v-else-if="
activeTab === ResponseComposition.HEADER ||
activeTab === ResponseComposition.REAL_REQUEST ||
activeTab === ResponseComposition.EXTRACT
"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<pre class="response-header-pre">{{ getResponsePreContent(activeTab) }}</pre>
<div v-show="props.requestResult?.responseResult.responseCode" class="h-full">
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]">
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
</a-tabs>
<div class="response-container">
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
show-language-change
show-charset-change
read-only
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<MsCodeEditor
v-else-if="activeTab === ResponseComposition.CONSOLE"
:model-value="props.console?.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
</MsCodeEditor>
<div
v-else-if="
activeTab === ResponseComposition.HEADER ||
activeTab === ResponseComposition.REAL_REQUEST ||
activeTab === ResponseComposition.EXTRACT
"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<pre class="response-header-pre">{{ getResponsePreContent(activeTab) }}</pre>
</div>
<MsBaseTable v-else-if="activeTab === 'ASSERTION'" v-bind="propsRes" v-on="propsEvent">
<template #status="{ record }">
<MsTag :type="record.status === 1 ? 'success' : 'danger'" theme="light">
{{ record.status === 1 ? t('common.success') : t('common.fail') }}
</MsTag>
</template>
</MsBaseTable>
</div>
<MsBaseTable v-else-if="activeTab === 'ASSERTION'" v-bind="propsRes" v-on="propsEvent">
<template #status="{ record }">
<MsTag :type="record.status === 1 ? 'success' : 'danger'" theme="light">
{{ record.status === 1 ? t('common.success') : t('common.fail') }}
</MsTag>
</template>
</MsBaseTable>
</div>
<a-empty
v-show="!props.requestResult?.responseResult.responseCode"
class="flex h-[150px] items-center gap-[16px] p-[16px]"
>
<template #image>
<img :src="noDataSvg" class="!h-[60px] w-[78px]" />
</template>
<div class="flex items-center gap-[8px]">
<div>{{ t('apiTestManagement.click') }}</div>
<MsButton
class="!mr-0"
type="text"
:disabled="props.isHttpProtocol && !props.requestUrl"
@click="emit('execute')"
>
{{ props.isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
</MsButton>
<div>{{ t('apiTestManagement.getResponse') }}</div>
</div>
</a-empty>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
@ -78,9 +101,15 @@
const props = defineProps<{
requestResult?: RequestResult;
console?: string;
isPriorityLocalExec: boolean;
requestUrl: string;
isHttpProtocol: boolean;
}>();
const emit = defineEmits(['execute']);
const { t } = useI18n();
const noDataSvg = `${import.meta.env.BASE_URL}images/noResponse.svg`;
const responseCompositionTabList = [
{
label: t('apiTestDebug.responseBody'),

View File

@ -34,8 +34,9 @@ export function parseRequestBodyFiles(
const tempSaveUploadFileIds = new Set<string>(); // 临时存储 body 内已保存的上传文件 id 集合,用于对比 saveUploadFileIds 以判断有哪些文件被删除
const tempSaveLinkFileIds = new Set<string>(); // 临时存储 body 内已保存的关联文件 id 集合,用于对比 saveLinkFileIds 以判断有哪些文件被取消关联
// 获取上传文件和关联文件
for (let i = 0; i < formDataBody.formValues.length; i++) {
const item = formDataBody.formValues[i];
const formValues = formDataBody?.formValues.filter((e) => e) || [];
for (let i = 0; i < formValues.length; i++) {
const item = formValues[i];
if (item.paramType === RequestParamsType.FILE) {
if (item.files) {
for (let j = 0; j < item.files.length; j++) {
@ -150,15 +151,15 @@ export function filterKeyValParams<T>(params: (T & Record<string, any>)[], defau
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,
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,
headers: filterKeyValParams(e.headers || [], defaultKeyValueParamItem).validParams,
})) || [],
};
}

View File

@ -77,13 +77,16 @@
@more-action-select="handleFolderMoreSelect"
@more-actions-close="moreActionsClose"
@drop="handleDrop"
@select="
(keys, node) => {
if (node.type === 'API') {
emit('clickApiNode', node);
}
}
"
>
<template #title="nodeData">
<div
v-if="nodeData.type === 'API'"
class="inline-flex w-full cursor-pointer gap-[4px]"
@click="emit('clickApiNode', nodeData)"
>
<div v-if="nodeData.type === 'API'" class="inline-flex w-full cursor-pointer gap-[4px]">
<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>
@ -101,7 +104,8 @@
:field-config="{ field: renameFolderTitle }"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:node-type="nodeData.type"
:add-module-api="addDebugModule"
:update-module-api="updateDebugModule"
:update-api-node-api="updateDebug"
@close="resetFocusNodeKey"
@rename-finish="handleRenameFinish"
>
@ -113,8 +117,7 @@
mode="add"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:parent-id="nodeData.id"
:update-module-api="updateDebugModule"
:update-api-node-api="updateDebug"
:add-module-api="addDebugModule"
@close="resetFocusNodeKey"
@add-finish="() => initModules()"
>

View File

@ -293,6 +293,7 @@
...res.request,
url: res.path,
name: res.name, // requestnamenull
moduleId: res.moduleId, // requestmoduleIdnull
...parseRequestBodyResult,
});
nextTick(() => {

View File

@ -425,7 +425,6 @@
}
return [props.activeModule];
});
const tableQueryParams = ref<any>();
function loadApiList() {
const params = {
keyword: keyword.value,
@ -440,11 +439,6 @@
};
setLoadListParams(params);
loadList();
tableQueryParams.value = {
...params,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
};
}
watch(

View File

@ -190,7 +190,7 @@
</a-tab-pane>
<a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane">
</a-tab-pane>
<a-tab-pane v-if="!activeApiTab.isNew" key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
<!-- <a-tab-pane v-if="!activeApiTab.isNew" key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane> -->
</a-tabs>
</div>
</div>
@ -198,7 +198,7 @@
</template>
<script setup lang="ts">
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
// import MsButton from '@/components/pure/ms-button/index.vue';
@ -249,7 +249,7 @@
const requestComposition = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/index.vue')
);
const preview = defineAsyncComponent(() => import('./preview.vue'));
const preview = defineAsyncComponent(() => import('./preview/index.vue'));
const props = defineProps<{
activeModule: string;

View File

@ -1,862 +0,0 @@
<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

@ -0,0 +1,728 @@
<template>
<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>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
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 MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.vue';
import { getPluginScript } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n';
import { PluginConfig, ProtocolItem } from '@/models/apiTest/common';
import { RequestBodyFormat, 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;
protocols: ProtocolItem[];
}>();
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 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();
border-radius: 0;
:deep(.arco-collapse-item-icon-hover) {
@apply !hidden;
}
:deep(.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);
}
}
.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);
}
}
}
.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

@ -0,0 +1,124 @@
<template>
<div class="history-container">
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiTestManagement.historyListTip') }}
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
</template>
</a-alert>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<!-- <template #action="{ record }">
<div class="flex items-center">
<MsButton type="text" @click="recover(record)">{{ t('apiTestManagement.recover') }}</MsButton>
</div>
</template> -->
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
// import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { operationHistory } from '@/api/modules/api-test/management';
import { operationTypeOptions } from '@/config/common';
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app';
const props = defineProps<{
sourceId: string | number;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const visitedKey = 'messageManagementRobotListTip';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const columns: MsTableColumn = [
{
title: 'apiTestManagement.changeOrder',
dataIndex: 'id',
width: 200,
},
{
title: 'apiTestManagement.type',
dataIndex: 'type',
slotName: 'type',
titleSlotName: 'typeFilter',
width: 100,
},
{
title: 'mockManagement.operationUser',
dataIndex: 'createUserName',
showTooltip: true,
width: 150,
},
{
title: 'apiTestManagement.updateTime',
dataIndex: 'updateTime',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
// {
// title: 'common.operation',
// slotName: 'action',
// dataIndex: 'operation',
// width: 50,
// },
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
operationHistory,
{
columns,
scroll: { x: '100%' },
selectable: false,
heightUsed: 374,
},
(item) => ({
...item,
type: t(operationTypeOptions.find((e) => e.value === item.type)?.label || ''),
createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
})
);
function loadHistory() {
setLoadListParams({
projectId: appStore.currentProjectId,
sourceId: props.sourceId,
});
loadList();
}
onBeforeMount(() => {
loadHistory();
});
// async function recover(record: any) {
// try {
// await recoverOperationHistory({
// id: record.id,
// sourceId: props.sourceId,
// });
// } catch (error) {
// // eslint-disable-next-line no-console
// console.log(error);
// }
// }
</script>
<style lang="less" scoped>
.history-container {
@apply h-full overflow-y-auto;
.ms-scroll-bar();
}
</style>

View File

@ -0,0 +1,188 @@
<template>
<div class="h-full w-full overflow-hidden">
<div class="px-[18px] pt-[16px]">
<MsDetailCard
:title="`【${previewDetail.num}】${previewDetail.name}`"
:description="description"
:simple-show-count="4"
>
<template #titleAppend>
<apiStatus :status="previewDetail.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="previewDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${previewDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
:size="14"
/>
{{ t(previewDetail.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="px-[18px] py-[16px]">
<detailTab :detail="previewDetail" :protocols="props.protocols" />
</a-tab-pane>
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
<quote :source-id="previewDetail.id" />
</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]">
<history :source-id="previewDetail.id" />
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import detailTab from './detail.vue';
import history from './history.vue';
import quote from './quote.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { toggleFollowDefinition } from '@/api/modules/api-test/management';
import { findNodeByKey } from '@/utils';
import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import { RequestMethods } from '@/enums/apiEnum';
import { getValidRequestTableParams } from '@/views/api-test/components/utils';
const props = defineProps<{
detail: RequestParam;
moduleTree: ModuleTreeNode[];
protocols: ProtocolItem[];
}>();
const emit = defineEmits(['updateFollow']);
const { copy, isSupported } = useClipboard();
const { t } = useI18n();
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
watchEffect(() => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
const tableParam = getValidRequestTableParams(previewDetail.value); // props.detail
previewDetail.value = {
...previewDetail.value,
body: {
...previewDetail.value.body,
formDataBody: {
formValues: tableParam.formDataBodyTableParams,
},
wwwFormBody: {
formValues: tableParam.wwwFormBodyTableParams,
},
},
headers: tableParam.headers,
rest: tableParam.rest,
query: tableParam.query,
responseDefinition: tableParam.response,
};
});
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: previewDetail.value.method,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: previewDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: previewDetail.value.tags,
},
{
key: 'description',
locale: 'common.desc',
value: previewDetail.value.description,
width: '100%',
},
{
key: 'belongModule',
locale: 'apiTestManagement.belongModule',
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path,
},
{
key: 'creator',
locale: 'common.creator',
value: previewDetail.value.createUserName,
},
{
key: 'createTime',
locale: 'apiTestManagement.createTime',
value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
key: 'updateTime',
locale: 'apiTestManagement.updateTime',
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
]);
const followLoading = ref(false);
async function toggleFollowReview() {
try {
followLoading.value = true;
await toggleFollowDefinition(previewDetail.value.id);
Message.success(previewDetail.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=${previewDetail.value.id}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
const activeKey = ref('detail');
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,136 @@
<template>
<div class="history-container">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiTestManagement.quoteSearchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadQuoteList"
@press-enter="loadQuoteList"
/>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #id="{ record }">
<MsButton type="text" @click="gotoResource(record)">{{ record.id }}</MsButton>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
// import { useRouter } from 'vue-router';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
const props = defineProps<{
sourceId: string | number;
}>();
// const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const keyword = ref('');
const quoteLocaleMap = {
COPY: 'common.copy',
QUOTE: 'apiTestManagement.quote',
};
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'id',
slotName: 'id',
width: 150,
},
{
title: 'apiTestManagement.resourceName',
dataIndex: 'resourceName',
showTooltip: true,
width: 150,
},
{
title: 'apiTestManagement.resourceType',
dataIndex: 'resourceType',
width: 100,
},
{
title: 'apiTestManagement.quoteType',
dataIndex: 'quoteType',
width: 100,
},
{
title: 'apiTestManagement.belongOrg',
dataIndex: 'belongOrg',
showTooltip: true,
width: 150,
},
{
title: 'apiTestManagement.belongProject',
dataIndex: 'belongProject',
showTooltip: true,
width: 150,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
() =>
Promise.resolve({
list: [
{
id: '1',
resourceName: '资源名称',
resourceType: '资源类型',
quoteType: '引用类型',
belongOrg: '所属组织',
belongProject: '所属项目',
},
],
total: 1,
}),
{
columns,
scroll: { x: '100%' },
selectable: false,
heightUsed: 374,
},
(item) => ({
...item,
type: t(quoteLocaleMap[item.type] || ''),
})
);
function loadQuoteList() {
setLoadListParams({
projectId: appStore.currentProjectId,
sourceId: props.sourceId,
keyword: keyword.value,
});
loadList();
}
function gotoResource(record: any) {
// router.push({
// name: 'apiTestManagementApiPreview',
// query: {
// id: record.id,
// },
// });
}
onBeforeMount(() => {
loadQuoteList();
});
</script>
<style lang="less" scoped>
.history-container {
@apply h-full overflow-y-auto;
.ms-scroll-bar();
}
</style>

View File

@ -10,7 +10,7 @@
/>
</a-tab-pane>
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane">
<!-- <a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane">
<mock-table
ref="mockRef"
:module-tree="props.moduleTree"
@ -18,7 +18,7 @@
:offspring-ids="props.offspringIds"
:protocol="protocol"
/>
</a-tab-pane>
</a-tab-pane> -->
<!-- <a-tab-pane key="doc" title="API Docs" class="ms-api-tab-pane"> </a-tab-pane> -->
<template #extra>
<div class="flex items-center gap-[8px] pr-[24px]">
@ -47,8 +47,8 @@
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 MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { getEnvironment, getEnvList } from '@/api/modules/api-test/common';
import useAppStore from '@/store/modules/app';

View File

@ -350,15 +350,12 @@
: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-brand);
}

View File

@ -103,11 +103,7 @@
@drop="handleDrop"
>
<template #title="nodeData">
<div
v-if="nodeData.type === 'API'"
class="inline-flex w-full cursor-pointer gap-[4px]"
@click="emit('clickApiNode', nodeData)"
>
<div v-if="nodeData.type === 'API'" class="inline-flex w-full cursor-pointer gap-[4px]">
<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>
@ -451,6 +447,8 @@
return e;
});
emit('folderNodeSelect', _selectedKeys, offspringIds);
} else if (node.type === 'API') {
emit('clickApiNode', node);
}
}

View File

@ -131,4 +131,17 @@ export default {
'apiTestManagement.paramsType': 'Param type',
'apiTestManagement.required': 'Required',
'apiTestManagement.requestData': 'Request data',
'apiTestManagement.apiNameRequired': 'Interface name cannot be empty',
'apiTestManagement.historyListTip':
'View and compare historical changes. According to the rules set by the administrator, the change history data will be automatically deleted.',
'apiTestManagement.changeOrder': 'Change serial number',
'apiTestManagement.type': 'Type',
'apiTestManagement.recover': 'Recover',
'apiTestManagement.quote': 'Quote',
'apiTestManagement.resourceName': 'Resource name',
'apiTestManagement.resourceType': 'Resource type',
'apiTestManagement.quoteType': 'Quote type',
'apiTestManagement.belongOrg': 'Organization',
'apiTestManagement.belongProject': 'Project',
'apiTestManagement.quoteSearchPlaceholder': 'Enter ID or name to search',
};

View File

@ -125,4 +125,18 @@ export default {
'apiTestManagement.paramsType': '参数类型',
'apiTestManagement.required': '必填',
'apiTestManagement.requestData': '请求数据',
'apiTestManagement.apiNameRequired': '接口名称不能为空',
'apiTestManagement.historyListTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'apiTestManagement.changeOrder': '变更序号',
'apiTestManagement.type': '类型',
'apiTestManagement.recover': '恢复',
'apiTestManagement.quote': '引用',
'apiTestManagement.resourceName': '资源名称',
'apiTestManagement.resourceType': '资源类型',
'apiTestManagement.quoteType': '引用类型',
'apiTestManagement.belongOrg': '所属组织',
'apiTestManagement.belongProject': '所属项目',
'apiTestManagement.quoteSearchPlaceholder': '输入 ID 或名称搜索',
'apiTestManagement.click': '点击',
'apiTestManagement.getResponse': '获取响应内容',
};

View File

@ -76,8 +76,8 @@
name: getFirstRouteNameByPermission(router.getRoutes()),
query: {
...route.query,
organizationId: appStore.currentOrgId,
projectId: appStore.currentProjectId,
orgId: appStore.currentOrgId,
pId: appStore.currentProjectId,
},
});
}

View File

@ -352,7 +352,7 @@
function shareHandler() {
const { origin } = window.location;
const url = `${origin}/#${route.path}?id=${detailInfo.value.id}&projectId=${appStore.currentProjectId}&organizationId=${appStore.currentOrgId}`;
const url = `${origin}/#${route.path}?id=${detailInfo.value.id}&pId=${appStore.currentProjectId}&orgId=${appStore.currentOrgId}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(url).then(
() => {

View File

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

View File

@ -414,7 +414,7 @@
function shareHandler() {
const { origin } = window.location;
const url = `${origin}/#${route.path}?id=${detailInfo.value.id}&projectId=${appStore.currentProjectId}&organizationId=${appStore.currentOrgId}`;
const url = `${origin}/#${route.path}?id=${detailInfo.value.id}&pId=${appStore.currentProjectId}&orgId=${appStore.currentOrgId}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(url).then(
() => {

View File

@ -166,8 +166,8 @@
name: redirectHasPermission ? (redirect as string) : currentRouteName,
query: {
...othersQuery,
organizationId: appStore.currentOrgId,
projectId: appStore.currentProjectId,
orgId: appStore.currentOrgId,
pId: appStore.currentProjectId,
},
});
} catch (err) {

View File

@ -552,15 +552,15 @@
function handleNameClick(record: LogItem) {
const routeQuery: Record<string, any> = {
organizationId: record.organizationId,
projectId: record.projectId,
orgId: record.organizationId,
pId: record.projectId,
id: record.sourceId,
};
if (record.organizationId === 'SYSTEM') {
delete routeQuery.organizationId;
delete routeQuery.orgId;
}
if (record.projectId === 'SYSTEM' || record.projectId === 'ORGANIZATION') {
delete routeQuery.projectId;
delete routeQuery.pId;
}
jumpRouteByMapKey(record.module, routeQuery, true);
}