feat(接口测试): 接口测试-请求体&代码编辑器支持切换主题

This commit is contained in:
baiqi 2023-12-22 14:36:52 +08:00 committed by 刘瑞斌
parent de2de8ce05
commit 1a7c1c2410
19 changed files with 1082 additions and 363 deletions

View File

@ -332,6 +332,7 @@
emit('dblclick'); emit('dblclick');
}); });
const autoCompleteInput = (autoCompleteRef.value?.inputRef as any)?.$el.querySelector('.arco-input'); const autoCompleteInput = (autoCompleteRef.value?.inputRef as any)?.$el.querySelector('.arco-input');
// popover
useEventListener(autoCompleteInput, 'focus', () => { useEventListener(autoCompleteInput, 'focus', () => {
isFocusAutoComplete.value = true; isFocusAutoComplete.value = true;
popoverVisible.value = false; popoverVisible.value = false;
@ -515,7 +516,7 @@
if (currentParamsFuncInputGroup.value.length > 0 && !Number.isNaN(paramForm.value.funcParam1)) { if (currentParamsFuncInputGroup.value.length > 0 && !Number.isNaN(paramForm.value.funcParam1)) {
// //
resultStr = `${resultStr}(${[paramForm.value.funcParam1, paramForm.value.funcParam2] resultStr = `${resultStr}(${[paramForm.value.funcParam1, paramForm.value.funcParam2]
.filter((e) => !Number.isNaN(e)) .filter((e) => e !== '' && !Number.isNaN(e))
.join(',')})`; .join(',')})`;
} }
} }
@ -579,6 +580,10 @@
.ms-params-input:not(.arco-input-focus) { .ms-params-input:not(.arco-input-focus) {
border-color: transparent; border-color: transparent;
&:not(:hover) { &:not(:hover) {
.arco-input::placeholder {
@apply invisible;
}
border-color: transparent; border-color: transparent;
} }
} }

View File

@ -4,6 +4,15 @@
<slot name="title"> <slot name="title">
<span class="font-medium">{{ title }}</span> <span class="font-medium">{{ title }}</span>
</slot> </slot>
<div v-if="showThemeChange">
<a-select
v-model:model-value="currentTheme"
:options="themeOptions"
class="w-[100px]"
size="small"
@change="(val) => handleThemeChange(val as Theme)"
></a-select>
</div>
<div v-if="showFullScreen" class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]" @click="toggle"> <div v-if="showFullScreen" class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]" @click="toggle">
<MsIcon v-if="isFullscreen" type="icon-icon_minify_outlined" /> <MsIcon v-if="isFullscreen" type="icon-icon_minify_outlined" />
<MsIcon v-else type="icon-icon_magnify_outlined" /> <MsIcon v-else type="icon-icon_magnify_outlined" />
@ -22,7 +31,7 @@
import './userWorker'; import './userWorker';
import MsCodeEditorTheme from './themes'; import MsCodeEditorTheme from './themes';
import { CustomTheme, editorProps } from './types'; import { CustomTheme, editorProps, Theme } from './types';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
export default defineComponent({ export default defineComponent({
@ -34,12 +43,34 @@
let editor: monaco.editor.IStandaloneCodeEditor; let editor: monaco.editor.IStandaloneCodeEditor;
const codeEditBox = ref(); const codeEditBox = ref();
const fullRef = ref<HTMLElement | null>(); const fullRef = ref<HTMLElement | null>();
const currentTheme = ref<Theme>(props.theme);
const themeOptions = [
{ label: 'vs', value: 'vs' },
{ label: 'vs-dark', value: 'vs-dark' },
{ label: 'hc-black', value: 'hc-black' },
].concat(
Object.keys(MsCodeEditorTheme).map((item) => ({
label: item,
value: item,
}))
);
watch(
() => props.theme,
(val) => {
currentTheme.value = val;
}
);
function handleThemeChange(val: Theme) {
monaco.editor.setTheme(val);
}
const init = () => { const init = () => {
// //
if (MsCodeEditorTheme[props.theme as CustomTheme]) { Object.keys(MsCodeEditorTheme).forEach((e) => {
monaco.editor.defineTheme(props.theme, MsCodeEditorTheme[props.theme as CustomTheme]); monaco.editor.defineTheme(e, MsCodeEditorTheme[e as CustomTheme]);
} });
editor = monaco.editor.create(codeEditBox.value, { editor = monaco.editor.create(codeEditBox.value, {
value: props.modelValue, value: props.modelValue,
automaticLayout: true, automaticLayout: true,
@ -90,12 +121,12 @@
{ deep: true } { deep: true }
); );
// watch( watch(
// () => props.language, () => props.language,
// (newValue) => { (newValue) => {
// monaco.editor.setModelLanguage(editor.getModel()!, newValue); monaco.editor.setModelLanguage(editor.getModel()!, newValue);
// } }
// ); );
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.dispose(); editor.dispose();
@ -106,7 +137,7 @@
setEditBoxBg(); setEditBoxBg();
}); });
return { codeEditBox, fullRef, isFullscreen, toggle, t }; return { codeEditBox, fullRef, isFullscreen, currentTheme, themeOptions, toggle, t, handleThemeChange };
}, },
}); });
</script> </script>

View File

@ -87,4 +87,8 @@ export const editorProps = {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: true, default: true,
}, },
showThemeChange: {
type: Boolean as PropType<boolean>,
default: true,
},
}; };

View File

@ -44,9 +44,20 @@
<MsIcon type="icon-icon_right_outlined" /> <MsIcon type="icon-icon_right_outlined" />
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<MsButton type="icon" status="secondary" class="tab-button !mr-[4px]" @click="addTab"> <a-tooltip
<MsIcon type="icon-icon_add_outlined" /> :content="t('ms.editableTab.limitTip', { max: props.limit })"
</MsButton> :disabled="!props.limit || props.tabs.length >= props.limit"
>
<MsButton
type="icon"
status="secondary"
class="tab-button !mr-[4px]"
:disabled="!!props.limit && props.tabs.length >= props.limit"
@click="addTab"
>
<MsIcon type="icon-icon_add_outlined" />
</MsButton>
</a-tooltip>
<MsMoreAction v-if="props.moreActionList" :list="props.moreActionList"> <MsMoreAction v-if="props.moreActionList" :list="props.moreActionList">
<MsButton type="icon" status="secondary" class="tab-button"> <MsButton type="icon" status="secondary" class="tab-button">
<MsIcon type="icon-icon_more_outlined" /> <MsIcon type="icon-icon_more_outlined" />
@ -72,12 +83,13 @@
tabs: TabItem[]; tabs: TabItem[];
activeTab: string | number; activeTab: string | number;
moreActionList?: ActionsItem[]; moreActionList?: ActionsItem[];
limit?: number; // tab
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:activeTab', activeTab: string | number): void; (e: 'update:activeTab', activeTab: string | number): void;
(e: 'add'): void; (e: 'add'): void;
(e: 'close', item: TabItem): void; (e: 'close', item: TabItem): void;
(e: 'click', item: TabItem): void; (e: 'change', item: TabItem): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -119,6 +131,13 @@
} }
}; };
watch(
() => props.activeTab,
(val) => {
emit('change', props.tabs.find((item) => item.id === val) as TabItem);
}
);
watch(props.tabs, () => { watch(props.tabs, () => {
nextTick(() => { nextTick(() => {
scrollToActiveTab(); scrollToActiveTab();
@ -141,6 +160,7 @@
} }
function handleTabClick(item: TabItem) { function handleTabClick(item: TabItem) {
emit('change', item);
innerActiveTab.value = item.id; innerActiveTab.value = item.id;
nextTick(() => { nextTick(() => {
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });

View File

@ -1,4 +1,5 @@
export default { export default {
'ms.editableTab.arrivedLeft': 'Already reached the far left~', 'ms.editableTab.arrivedLeft': 'Already reached the far left~',
'ms.editableTab.arrivedRight': 'Already reached the far right~', 'ms.editableTab.arrivedRight': 'Already reached the far right~',
'ms.editableTab.limitTip': 'Up to {max} tabs can currently be open',
}; };

View File

@ -1,4 +1,5 @@
export default { export default {
'ms.editableTab.arrivedLeft': '到最左侧啦~', 'ms.editableTab.arrivedLeft': '到最左侧啦~',
'ms.editableTab.arrivedRight': '到最右侧啦~', 'ms.editableTab.arrivedRight': '到最右侧啦~',
'ms.editableTab.limitTip': '当前最多可打开 {max} 个标签页',
}; };

View File

@ -162,7 +162,7 @@
.ms-scroll-bar(); .ms-scroll-bar();
} }
.ms-split-box--left { .ms-split-box--left {
width: calc(v-bind(innerSize) - 4px); width: calc(v-bind(innerSize) - 2px);
} }
.expand-icon { .expand-icon {
@apply relative z-20 flex cursor-pointer justify-center; @apply relative z-20 flex cursor-pointer justify-center;
@ -185,7 +185,7 @@
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0; border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
} }
.horizontal-expand-line { .horizontal-expand-line {
padding: 0 1px; padding-left: 2px;
height: 100%; height: 100%;
.expand-color-line { .expand-color-line {
width: 1px; width: 1px;

View File

@ -392,22 +392,22 @@ export default function useTableProps<T>(
}); });
watchEffect(() => { watchEffect(() => {
if (props?.heightUsed) { const { heightUsed, showPagination, selectedKeys, msPagination } = propsRes.value;
const { heightUsed, showPagination, selectedKeys, msPagination } = propsRes.value; let hasFooterAction = false;
let hasFooterAction = false; if (showPagination) {
if (showPagination) { const { pageSize, total } = msPagination as Pagination;
const { pageSize, total } = msPagination as Pagination; /*
/* *
* * 1.
* 1. * 2.
* 2. */
*/ hasFooterAction = total > pageSize || selectedKeys.size > 0;
hasFooterAction = total > pageSize || selectedKeys.size > 0; }
}
propsRes.value.showFooterActionWrap = hasFooterAction;
if (props?.heightUsed) {
const currentY = const currentY =
appStore.innerHeight - (heightUsed || defaultHeightUsed) + (hasFooterAction ? 0 : footerActionWrapHeight); appStore.innerHeight - (heightUsed || defaultHeightUsed) + (hasFooterAction ? 0 : footerActionWrapHeight);
propsRes.value.showFooterActionWrap = hasFooterAction;
propsRes.value.scroll = { ...propsRes.value.scroll, y: currentY }; propsRes.value.scroll = { ...propsRes.value.scroll, y: currentY };
} }
}); });

View File

@ -1,3 +1,4 @@
// 接口请求方法
export enum RequestMethods { export enum RequestMethods {
GET = 'GET', GET = 'GET',
POST = 'POST', POST = 'POST',
@ -8,7 +9,7 @@ export enum RequestMethods {
HEAD = 'HEAD', HEAD = 'HEAD',
CONNECT = 'CONNECT', CONNECT = 'CONNECT',
} }
// 接口组成部分
export enum RequestComposition { export enum RequestComposition {
HEADER = 'HEADER', HEADER = 'HEADER',
BODY = 'BODY', BODY = 'BODY',
@ -20,3 +21,25 @@ export enum RequestComposition {
AUTH = 'AUTH', AUTH = 'AUTH',
SETTING = 'SETTING', SETTING = 'SETTING',
} }
// 接口请求体格式
export enum RequestBodyFormat {
NONE = 'none',
FORM_DATA = 'form-data',
X_WWW_FORM_URLENCODED = 'x-www-form-urlencoded',
JSON = 'json',
XML = 'xml',
RAW = 'raw',
BINARY = 'binary',
}
// 接口响应体格式
export enum RequestContentTypeEnum {
JSON = 'application/json',
TEXT = 'application/text',
OGG = 'application/ogg',
PDF = 'application/pdf',
JAVASCRIPT = 'application/javascript',
OCTET_STREAM = 'application/octet-stream',
VND_API_JSON = 'application/vnd.api+json',
ATOM_XML = 'application/atom+xml',
ECMASCRIPT = 'application/ecmascript',
}

View File

@ -8,7 +8,13 @@
{{ props.desc }} {{ props.desc }}
</div> </div>
</template> </template>
<a-input ref="inputRef" v-model:model-value="innerValue" class="param-input" @input="(val) => emit('input', val)" /> <a-input
ref="inputRef"
v-model:model-value="innerValue"
class="param-input"
@input="(val) => emit('input', val)"
@change="(val) => emit('change', val)"
/>
</a-popover> </a-popover>
</template> </template>
@ -21,8 +27,9 @@
desc: string; desc: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:value', val: string): void; (e: 'update:desc', val: string): void;
(e: 'input', val: string): void; (e: 'input', val: string): void;
(e: 'change', val: string): void;
(e: 'dblclick'): void; (e: 'dblclick'): void;
}>(); }>();

View File

@ -0,0 +1,466 @@
<template>
<MsBaseTable v-bind="propsRes" id="headerTable" :hoverable="false" v-on="propsEvent">
<template #encodeTitle>
<div class="flex items-center text-[var(--color-text-3)]">
{{ t('ms.apiTestDebug.encode') }}
<a-tooltip>
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div>{{ t('ms.apiTestDebug.encodeTip1') }}</div>
<div>{{ t('ms.apiTestDebug.encodeTip2') }}</div>
</template>
</a-tooltip>
</div>
</template>
<template #name="{ record }">
<a-popover position="tl" :disabled="!record.name || record.name.trim() === ''" class="ms-params-input-popover">
<template #content>
<div class="param-popover-title">
{{ t('ms.apiTestDebug.paramName') }}
</div>
<div class="param-popover-value">
{{ record.name }}
</div>
</template>
<a-input
v-model:model-value="record.name"
:placeholder="t('ms.apiTestDebug.paramNamePlaceholder')"
class="param-input"
@input="(val) => addTableLine(val)"
/>
</a-popover>
</template>
<template #type="{ record }">
<a-tooltip :content="t(record.required ? 'ms.apiTestDebug.paramRequired' : 'ms.apiTestDebug.paramNotRequired')">
<MsButton
type="icon"
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
@click="record.required = !record.required"
>
<div>*</div>
</MsButton>
</a-tooltip>
<a-select
v-model:model-value="record.type"
:options="typeOptions"
class="param-input"
@change="(val) => handleTypeChange(val, record)"
></a-select>
</template>
<template #value="{ record }">
<MsParamsInput
v-model:value="record.value"
@change="addTableLine"
@dblclick="quickInputParams(record)"
@apply="handleParamSettingApply"
/>
</template>
<template #lengthRange="{ record }">
<div class="flex items-center justify-between">
<a-input-number
v-model:model-value="record.min"
:placeholder="t('ms.apiTestDebug.paramMin')"
class="param-input"
@input="(val) => addTableLine(val)"
></a-input-number>
<div class="mx-[4px]"></div>
<a-input-number
v-model:model-value="record.max"
:placeholder="t('ms.apiTestDebug.paramMax')"
class="param-input"
@input="(val) => addTableLine(val)"
></a-input-number>
</div>
</template>
<template #desc="{ record }">
<paramDescInput
v-model:desc="record.desc"
@input="addTableLine"
@dblclick="quickInputDesc(record)"
@change="handleDescChange"
/>
</template>
<template #encode="{ record }">
<a-switch
v-model:model-value="record.encode"
size="small"
class="param-input-switch"
@change="(val) => addTableLine(val.toString())"
></a-switch>
</template>
<template #operation="{ record, rowIndex }">
<a-trigger
v-if="props.format && props.format !== RequestBodyFormat.X_WWW_FORM_URLENCODED"
trigger="click"
position="br"
>
<MsButton type="icon" class="mr-[8px]"><icon-more /></MsButton>
<template #content>
<div class="content-type-trigger-content">
<div class="mb-[8px] text-[var(--color-text-1)]">Content-Type</div>
<a-select
v-model:model-value="record.contentType"
:options="Object.values(RequestContentTypeEnum).map((e) => ({ label: e, value: e }))"
allow-create
@change="(val) => addTableLine(val as string)"
></a-select>
</div>
</template>
</a-trigger>
<icon-minus-circle
v-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
class="cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(rowIndex)"
/>
</template>
</MsBaseTable>
<a-modal
v-model:visible="showQuickInputParam"
:title="t('ms.paramsInput.value')"
:ok-text="t('ms.apiTestDebug.apply')"
class="ms-modal-form"
body-class="!p-0"
:width="680"
title-align="start"
@ok="applyQuickInputParam"
@close="clearQuickInputParam"
>
<MsCodeEditor
v-if="showQuickInputParam"
v-model:model-value="quickInputParamValue"
theme="MS-text"
height="300px"
:show-full-screen="false"
>
<template #title>
<div class="flex justify-between">
<div class="text-[var(--color-text-1)]">
{{ t('ms.apiTestDebug.quickInputParamsTip') }}
</div>
</div>
</template>
</MsCodeEditor>
</a-modal>
<a-modal
v-model:visible="showQuickInputDesc"
:title="t('ms.apiTestDebug.desc')"
:ok-text="t('common.save')"
:ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }"
class="ms-modal-form"
body-class="!p-0"
:width="480"
title-align="start"
:auto-size="{ minRows: 2 }"
@ok="applyQuickInputDesc"
@close="clearQuickInputDesc"
>
<a-textarea
v-model:model-value="quickInputDescValue"
:placeholder="t('ms.apiTestDebug.descPlaceholder')"
:max-length="255"
show-word-limit
></a-textarea>
</a-modal>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsParamsInput from '@/components/business/ms-params-input/index.vue';
import paramDescInput from './paramDescInput.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestBodyFormat, RequestContentTypeEnum } from '@/enums/apiEnum';
interface Param {
id: number;
required: boolean;
name: string;
type: string;
value: string;
min: number | undefined;
max: number | undefined;
contentType: RequestContentTypeEnum;
desc: string;
encode: boolean;
[key: string]: any;
}
const props = defineProps<{
params: any[];
columns: MsTableColumn;
format?: RequestBodyFormat;
scroll?: {
x?: number | string;
y?: number | string;
maxHeight?: number | string;
minWidth?: number | string;
};
heightUsed?: number;
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change', data: any[], isInit?: boolean): void;
}>();
const { t } = useI18n();
const defaultParams: Omit<Param, 'id'> = {
required: false,
name: '',
type: 'string',
value: '',
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,
desc: '',
encode: false,
};
const allType = [
{
label: 'string',
value: 'string',
},
{
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
{
label: 'json',
value: 'json',
},
{
label: 'file',
value: 'file',
},
];
const typeOptions = computed(() => {
if (props.format === RequestBodyFormat.X_WWW_FORM_URLENCODED) {
return allType.filter((e) => e.value !== 'file' && e.value !== 'json');
}
return allType;
});
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: props.scroll,
heightUsed: props.heightUsed,
columns: props.columns,
selectable: true,
draggable: { type: 'handle', width: 24 },
});
watch(
() => props.params,
(val) => {
if (val.length > 0) {
propsRes.value.data = val;
} else {
propsRes.value.data = props.params.concat({
id: new Date().getTime(),
required: false,
name: '',
type: 'string',
value: '',
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,
desc: '',
encode: false,
});
emit('change', propsRes.value.data, true);
}
},
{
immediate: true,
}
);
watch(
() => props.heightUsed,
(val) => {
propsRes.value.heightUsed = val;
}
);
const paramsLength = computed(() => propsRes.value.data.length);
function deleteParam(rowIndex: number) {
propsRes.value.data.splice(rowIndex, 1);
emit('change', propsRes.value.data);
}
/**
* 当表格输入框变化时给参数表格添加一行数据行
* @param val 输入值
* @param isForce 是否强制添加
*/
function addTableLine(val?: string | number, isForce?: boolean) {
const lastData = propsRes.value.data[propsRes.value.data.length - 1];
const isNotChange = Object.keys(defaultParams).every((key) => lastData[key] === defaultParams[key as any]);
if (isForce || (val !== '' && val !== undefined && !isNotChange)) {
propsRes.value.data.push({
id: new Date().getTime(),
...defaultParams,
} as any);
emit('change', propsRes.value.data);
}
}
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');
function quickInputParams(record: any) {
activeQuickInputRecord.value = record;
showQuickInputParam.value = true;
quickInputParamValue.value = record.value;
}
function clearQuickInputParam() {
activeQuickInputRecord.value = {};
quickInputParamValue.value = '';
}
function applyQuickInputParam() {
activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false;
clearQuickInputParam();
addTableLine(quickInputParamValue.value, true);
emit('change', propsRes.value.data);
}
function handleParamSettingApply(val: string | number) {
addTableLine(val);
}
const showQuickInputDesc = ref(false);
const quickInputDescValue = ref('');
function quickInputDesc(record: any) {
activeQuickInputRecord.value = record;
showQuickInputDesc.value = true;
quickInputDescValue.value = record.desc;
}
function clearQuickInputDesc() {
activeQuickInputRecord.value = {};
quickInputDescValue.value = '';
}
function applyQuickInputDesc() {
activeQuickInputRecord.value.desc = quickInputDescValue.value;
showQuickInputDesc.value = false;
clearQuickInputDesc();
addTableLine(quickInputDescValue.value, true);
emit('change', propsRes.value.data);
}
function handleDescChange() {
emit('change', propsRes.value.data);
}
function handleTypeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[],
record: Param
) {
addTableLine(val as string);
if (val === 'file') {
record.contentType = RequestContentTypeEnum.OCTET_STREAM;
} else if (val === 'json') {
record.contentType = RequestContentTypeEnum.JSON;
} else {
record.contentType = RequestContentTypeEnum.TEXT;
}
}
</script>
<style lang="less" scoped>
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-table-cell-align-left) {
padding: 16px 4px;
}
:deep(.arco-table-cell) {
padding: 11px 4px;
}
: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);
}
}
}
.param-input-switch:not(:hover).arco-switch-checked {
background-color: rgb(var(--primary-3)) !important;
}
.content-type-trigger-content {
@apply bg-white;
padding: 8px;
border-radius: var(--border-radius-small);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
}
.param-input {
.param-input-mock-icon {
@apply invisible;
}
&:hover,
&.arco-input-focus {
.param-input-mock-icon {
@apply visible cursor-pointer;
&:hover {
color: rgb(var(--primary-5));
}
}
}
}
.param-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-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,103 @@
<template>
<a-button type="outline" size="mini" @click="showBatchAddParamDrawer = true">
{{ t('ms.apiTestDebug.batchAdd') }}
</a-button>
<MsDrawer
v-model:visible="showBatchAddParamDrawer"
:title="t('common.batchAdd')"
:width="680"
:ok-text="t('ms.apiTestDebug.apply')"
disabled-width-drag
@confirm="applyBatchParams"
>
<div class="flex h-full">
<MsCodeEditor
v-if="showBatchAddParamDrawer"
v-model:model-value="batchParamsCode"
class="flex-1"
theme="MS-text"
height="calc(100% - 48px)"
:show-full-screen="false"
>
<template #title>
<div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip') }}
</div>
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip2') }}
</div>
</div>
</template>
</MsCodeEditor>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestContentTypeEnum } from '@/enums/apiEnum';
const props = defineProps<{
params: Record<string, any>[];
}>();
const emit = defineEmits<{
(e: 'apply', resultArr: (Record<string, any> | null)[]): void;
}>();
const { t } = useI18n();
const showBatchAddParamDrawer = ref(false);
const batchParamsCode = ref('');
watch(
() => showBatchAddParamDrawer.value,
(val) => {
if (val) {
batchParamsCode.value = props.params
.filter((e) => e && (e.name !== '' || e.value !== ''))
.map((item) => `${item.name}:${item.value}`)
.join('\n');
}
},
{
immediate: true,
}
);
/**
* 批量参数代码转换为参数表格数据
*/
function applyBatchParams() {
const arr = batchParamsCode.value.replaceAll('\r', '\n').split('\n'); //
const resultArr = arr
.map((item, i) => {
const [name, value] = item.split(':');
if (name || value) {
return {
id: new Date().getTime() + i,
name: name?.trim(),
value: value?.trim(),
required: false,
type: 'string',
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,
desc: '',
encode: false,
};
}
return null;
})
.filter((item) => item);
showBatchAddParamDrawer.value = false;
batchParamsCode.value = '';
emit('apply', resultArr);
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,222 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('ms.apiTestDebug.body') }}</div>
<div class="flex items-center gap-[16px]">
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
<a-radio-group v-model:model-value="format" type="button" size="small" @change="formatChange">
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">{{ item }}</a-radio>
</a-radio-group>
</div>
</div>
<div
v-if="format === 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('ms.apiTestDebug.noneBody') }}
</div>
<paramTable
v-else-if="showParamTable"
v-model:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:format="format"
:columns="columns"
:height-used="heightUsed"
@change="handleParamTableChange"
/>
<div v-else class="flex h-[calc(100%-100px)]">
<MsCodeEditor
v-model:model-value="currentBodyCode"
class="flex-1"
theme="vs-dark"
height="calc(100% - 48px)"
:show-full-screen="false"
:language="currentCodeLanguage"
>
<template #title>
<div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip') }}
</div>
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip2') }}
</div>
</div>
</template>
</MsCodeEditor>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import paramTable from '../../../components/paramTable.vue';
import batchAddKeyVal from './batchAddKeyVal.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestBodyFormat } from '@/enums/apiEnum';
export interface BodyParams {
format: RequestBodyFormat;
formData: Record<string, any>[];
formUrlEncode: Record<string, any>[];
json: string;
xml: string;
binary: string;
raw: string;
}
const props = defineProps<{
params: BodyParams;
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const columns: MsTableColumn = [
{
title: 'ms.apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
},
{
title: 'ms.apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
width: 120,
},
{
title: 'ms.apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
width: 240,
},
{
title: 'ms.apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
width: 200,
},
{
title: 'ms.apiTestDebug.desc',
dataIndex: 'desc',
slotName: 'desc',
},
{
title: 'ms.apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
titleSlotName: 'encodeTitle',
},
{
title: '',
slotName: 'operation',
fixed: 'right',
width: 80,
},
];
const heightUsed = ref<number | undefined>(undefined);
watch(
() => props.layout,
(val) => {
heightUsed.value = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight;
},
{
immediate: true,
}
);
watch(
() => props.secondBoxHeight,
(val) => {
if (props.layout === 'vertical') {
heightUsed.value = 422 + val;
}
},
{
immediate: true,
}
);
const format = ref(RequestBodyFormat.NONE);
const showParamTable = computed(() => {
return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.X_WWW_FORM_URLENCODED].includes(format.value);
});
const currentTableParams = computed({
get() {
if (format.value === RequestBodyFormat.FORM_DATA) {
return innerParams.value.formData;
}
return innerParams.value.formUrlEncode;
},
set(val) {
if (format.value === RequestBodyFormat.FORM_DATA) {
innerParams.value.formData = val;
} else {
innerParams.value.formUrlEncode = val;
}
},
});
const currentBodyCode = computed({
get() {
if (format.value === RequestBodyFormat.JSON) {
return innerParams.value.json;
}
if (format.value === RequestBodyFormat.XML) {
return innerParams.value.xml;
}
return innerParams.value.raw;
},
set(val) {
if (format.value === RequestBodyFormat.JSON) {
innerParams.value.json = val;
} else if (format.value === RequestBodyFormat.XML) {
innerParams.value.xml = val;
} else {
innerParams.value.raw = val;
}
},
});
const currentCodeLanguage = computed(() => {
if (format.value === RequestBodyFormat.JSON) {
return 'json';
}
if (format.value === RequestBodyFormat.XML) {
return 'xml';
}
return 'plaintext';
});
function formatChange() {
console.log('formatChange', format.value);
}
/**
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < currentTableParams.value.length) {
currentTableParams.value.splice(0, currentTableParams.value.length - 1, ...resultArr);
} else {
currentTableParams.value = [...resultArr, currentTableParams.value[currentTableParams.value.length - 1]];
}
emit('change');
}
function handleParamTableChange(resultArr: any[]) {
currentTableParams.value = [...resultArr];
emit('change');
}
</script>
<style lang="less" scoped></style>

View File

@ -1,131 +1,23 @@
<template> <template>
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('ms.apiTestDebug.header') }}</div> <div class="font-medium">{{ t('ms.apiTestDebug.header') }}</div>
<a-button type="outline" size="mini" @click="showBatchAddParamDrawer = true"> <batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
{{ t('ms.apiTestDebug.batchAdd') }}
</a-button>
</div> </div>
<div class="relative"> <paramTable
<MsBaseTable v-bind="propsRes" id="headerTable" v-on="propsEvent"> v-model:params="innerParams"
<template #name="{ record }"> :columns="columns"
<a-popover position="tl" :disabled="!record.name || record.name.trim() === ''" class="ms-params-input-popover"> :height-used="heightUsed"
<template #content> :scroll="scroll"
<div class="param-popover-title"> @change="handleParamTableChange"
{{ t('ms.apiTestDebug.paramName') }} />
</div>
<div class="param-popover-value">
{{ record.name }}
</div>
</template>
<a-input
v-model:model-value="record.name"
:placeholder="t('ms.apiTestDebug.paramNamePlaceholder')"
class="param-input"
@input="addTableLine"
/>
</a-popover>
</template>
<template #value="{ record }">
<MsParamsInput
v-model:value="record.value"
@change="addTableLine"
@dblclick="quickInputParams(record)"
@apply="handleParamSettingApply"
/>
</template>
<template #desc="{ record }">
<paramDescInput v-model:desc="record.desc" @input="addTableLine" @dblclick="quickInputDesc(record)" />
</template>
<template #operation="{ rowIndex }">
<icon-minus-circle
v-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
class="cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(rowIndex)"
/>
</template>
</MsBaseTable>
</div>
<MsDrawer
v-model:visible="showBatchAddParamDrawer"
:title="t('common.batchAdd')"
:width="680"
:ok-text="t('ms.apiTestDebug.apply')"
disabled-width-drag
@confirm="applyBatchParams"
>
<div class="flex h-full">
<MsCodeEditor
v-model:model-value="batchParamsCode"
class="flex-1"
theme="MS-text"
height="calc(100% - 48px)"
:show-full-screen="false"
>
<template #title>
<div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip') }}
</div>
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip2') }}
</div>
</div>
</template>
</MsCodeEditor>
</div>
</MsDrawer>
<a-modal
v-model:visible="showQuickInputParam"
:title="t('ms.paramsInput.value')"
:ok-text="t('ms.apiTestDebug.apply')"
class="ms-modal-form"
body-class="!p-0"
:width="680"
title-align="start"
@ok="applyQuickInputParam"
@close="clearQuickInputParam"
>
<MsCodeEditor v-model:model-value="quickInputParamValue" theme="MS-text" height="300px" :show-full-screen="false">
<template #title>
<div class="flex justify-between">
<div class="text-[var(--color-text-1)]">
{{ t('ms.apiTestDebug.quickInputParamsTip') }}
</div>
</div>
</template>
</MsCodeEditor>
</a-modal>
<a-modal
v-model:visible="showQuickInputDesc"
:title="t('ms.apiTestDebug.desc')"
:ok-text="t('common.save')"
:ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }"
class="ms-modal-form"
body-class="!p-0"
:width="480"
title-align="start"
:auto-size="{ minRows: 2 }"
@ok="applyQuickInputDesc"
@close="clearQuickInputDesc"
>
<a-textarea
v-model:model-value="quickInputDescValue"
:placeholder="t('ms.apiTestDebug.descPlaceholder')"
:max-length="255"
show-word-limit
></a-textarea>
</a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; import { useVModel } from '@vueuse/core';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type'; import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import paramTable from '../../../components/paramTable.vue';
import MsParamsInput from '@/components/business/ms-params-input/index.vue'; import batchAddKeyVal from './batchAddKeyVal.vue';
import paramDescInput from './paramDescInput.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -134,9 +26,15 @@
layout: 'horizontal' | 'vertical'; layout: 'horizontal' | 'vertical';
secondBoxHeight: number; secondBoxHeight: number;
}>(); }>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change'): void; //
}>();
const { t } = useI18n(); const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ms.apiTestDebug.paramName', title: 'ms.apiTestDebug.paramName',
@ -159,25 +57,14 @@
width: 50, width: 50,
}, },
]; ];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: props.layout === 'horizontal' ? { x: '700px' } : { x: '100%' },
heightUsed: props.layout === 'horizontal' ? 422 : 422 + props.secondBoxHeight,
columns,
selectable: true,
draggable: { type: 'handle', width: 24 },
});
propsRes.value.data = props.params.concat({ const heightUsed = ref<number | undefined>(undefined);
id: new Date().getTime(), const scroll = computed(() => (props.layout === 'horizontal' ? { x: '700px' } : { x: '100%' }));
name: '',
value: '',
desc: '',
});
watch( watch(
() => props.layout, () => props.layout,
(val) => { (val) => {
propsRes.value.heightUsed = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight; heightUsed.value = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight;
}, },
{ {
immediate: true, immediate: true,
@ -188,7 +75,7 @@
() => props.secondBoxHeight, () => props.secondBoxHeight,
(val) => { (val) => {
if (props.layout === 'vertical') { if (props.layout === 'vertical') {
propsRes.value.heightUsed = 422 + val; heightUsed.value = 422 + val;
} }
}, },
{ {
@ -196,150 +83,24 @@
} }
); );
const paramsLength = computed(() => propsRes.value.data.length);
function deleteParam(rowIndex: number) {
propsRes.value.data.splice(rowIndex, 1);
}
/**
* 当表格输入框变化时给参数表格添加一行数据行
* @param val 输入值
*/
function addTableLine(val: string | number) {
const lastData = propsRes.value.data[propsRes.value.data.length - 1];
if (val && (lastData.name || lastData.value || lastData.desc)) {
propsRes.value.data.push({
id: new Date().getTime(),
name: '',
value: '',
desc: '',
} as any);
}
}
const showBatchAddParamDrawer = ref(false);
const batchParamsCode = ref('');
/** /**
* 批量参数代码转换为参数表格数据 * 批量参数代码转换为参数表格数据
*/ */
function applyBatchParams() { function handleBatchParamApply(resultArr: any[]) {
const arr = batchParamsCode.value.replaceAll('\r', '\n').split('\n'); // if (resultArr.length < innerParams.value.length) {
const resultArr = arr innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
.map((item, i) => { } else {
const [name, value] = item.split(':'); innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
if (name && value) { }
return { emit('change');
id: new Date().getTime() + i,
name: name.trim(),
value: value.trim(),
desc: '',
};
}
return null;
})
.filter((item) => item);
propsRes.value.data.splice(propsRes.value.data.length - 1, 0, ...(resultArr as any[]));
showBatchAddParamDrawer.value = false;
batchParamsCode.value = '';
} }
const showQuickInputParam = ref(false); function handleParamTableChange(resultArr: any[], isInit?: boolean) {
const activeQuickInputRecord = ref<any>({}); innerParams.value = [...resultArr];
const quickInputParamValue = ref(''); if (!isInit) {
emit('change');
function quickInputParams(record: any) { }
activeQuickInputRecord.value = record;
showQuickInputParam.value = true;
quickInputParamValue.value = record.value;
}
function clearQuickInputParam() {
activeQuickInputRecord.value = {};
quickInputParamValue.value = '';
}
function applyQuickInputParam() {
activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false;
clearQuickInputParam();
}
function handleParamSettingApply(val: string | number) {
addTableLine(val);
}
const showQuickInputDesc = ref(false);
const quickInputDescValue = ref('');
function quickInputDesc(record: any) {
activeQuickInputRecord.value = record;
showQuickInputDesc.value = true;
quickInputDescValue.value = record.desc;
}
function clearQuickInputDesc() {
activeQuickInputRecord.value = {};
quickInputDescValue.value = '';
}
function applyQuickInputDesc() {
activeQuickInputRecord.value.desc = quickInputDescValue.value;
showQuickInputDesc.value = false;
clearQuickInputDesc();
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped></style>
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-table-cell-align-left) {
padding: 16px 4px;
}
:deep(.arco-table-cell) {
padding: 11px 4px;
}
.param-input:not(.arco-input-focus) {
&:not(:hover) {
border-color: transparent;
}
}
.param-input {
.param-input-mock-icon {
@apply invisible;
}
&:hover,
&.arco-input-focus {
.param-input-mock-icon {
@apply visible cursor-pointer;
&:hover {
color: rgb(var(--primary-5));
}
}
}
}
.param-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -6,28 +6,40 @@
:more-action-list="moreActionList" :more-action-list="moreActionList"
@add="addDebugTab" @add="addDebugTab"
@close="closeDebugTab" @close="closeDebugTab"
@click="setActiveDebug" @change="setActiveDebug"
> >
<template #label="{ tab }"> <template #label="{ tab }">
<apiMethodName :method="tab.method" class="mr-[4px]" /> <apiMethodName :method="tab.method" class="mr-[4px]" />
{{ tab.label }} {{ tab.label }}
<div class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div> <div v-if="tab.unSave" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
</template> </template>
</MsEditableTab> </MsEditableTab>
</div> </div>
<div class="px-[24px] pt-[16px]"> <div class="px-[24px] pt-[16px]">
<div class="mb-[4px] flex items-center justify-between"> <div class="mb-[4px] flex items-center justify-between">
<a-input-group class="flex-1"> <div class="flex flex-1">
<a-select v-model:model-value="activeDebug.method" class="w-[140px]"> <a-select
<template #label="{ data }"> v-model:model-value="activeDebug.moduleProtocol"
<apiMethodName :method="data.value" class="inline-block" /> :options="moduleProtocolOptions"
</template> class="mr-[4px] w-[90px]"
<a-option v-for="method of RequestMethods" :key="method" :value="method"> @change="handleActiveDebugChange"
<apiMethodName :method="method" /> />
</a-option> <a-input-group class="flex-1">
</a-select> <a-select v-model:model-value="activeDebug.method" class="w-[140px]" @change="handleActiveDebugChange">
<a-input v-model:model-value="debugUrl" :placeholder="t('ms.apiTestDebug.urlPlaceholder')" /> <template #label="{ data }">
</a-input-group> <apiMethodName :method="data.value" class="inline-block" />
</template>
<a-option v-for="method of RequestMethods" :key="method" :value="method">
<apiMethodName :method="method" />
</a-option>
</a-select>
<a-input
v-model:model-value="debugUrl"
:placeholder="t('ms.apiTestDebug.urlPlaceholder')"
@change="handleActiveDebugChange"
/>
</a-input-group>
</div>
<div class="ml-[16px]"> <div class="ml-[16px]">
<a-dropdown-button class="exec-btn"> <a-dropdown-button class="exec-btn">
{{ t('ms.apiTestDebug.serverExec') }} {{ t('ms.apiTestDebug.serverExec') }}
@ -57,12 +69,25 @@
@expand-change="handleExpandChange" @expand-change="handleExpandChange"
> >
<template #first> <template #first>
<div :class="`h-full min-w-[500px] px-[24px] pb-[16px] ${activeLayout === 'horizontal' ? ' pr-[16px]' : ''}`"> <div :class="`h-full min-w-[700px] px-[24px] pb-[16px] ${activeLayout === 'horizontal' ? ' pr-[16px]' : ''}`">
<a-tabs v-model:active-key="contentTab" class="no-content"> <a-tabs v-model:active-key="activeDebug.activeTab" class="no-content">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" /> <a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs> </a-tabs>
<a-divider margin="0" class="!mb-[16px]"></a-divider> <a-divider margin="0" class="!mb-[16px]"></a-divider>
<debugHeader :params="activeDebug.params" :layout="activeLayout" :second-box-height="secondBoxHeight" /> <debugHeader
v-if="activeDebug.activeTab === RequestComposition.HEADER"
v-model:params="activeDebug.headerParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugBody
v-else-if="activeDebug.activeTab === RequestComposition.BODY"
v-model:params="activeDebug.bodyParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
</div> </div>
</template> </template>
<template #second> <template #second>
@ -99,31 +124,45 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { debounce } from 'lodash-es'; import { cloneDeep, debounce } 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 MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import apiMethodName from '../../../components/apiMethodName.vue'; import apiMethodName from '../../../components/apiMethodName.vue';
import debugBody, { BodyParams } from './body.vue';
import debugHeader from './header.vue'; import debugHeader from './header.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { RequestComposition, RequestMethods } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestComposition, RequestMethods } from '@/enums/apiEnum';
const { t } = useI18n(); const { t } = useI18n();
const initDefaultId = `debug-${Date.now()}`; const initDefaultId = `debug-${Date.now()}`;
const activeTab = ref<string | number>(initDefaultId); const activeTab = ref<string | number>(initDefaultId);
const defaultBodyParams: BodyParams = {
format: RequestBodyFormat.NONE,
formData: [],
formUrlEncode: [],
json: '',
xml: '',
binary: '',
raw: '',
};
const debugTabs = ref<TabItem[]>([ const debugTabs = ref<TabItem[]>([
{ {
id: initDefaultId, id: initDefaultId,
moduleProtocol: 'http',
activeTab: RequestComposition.HEADER,
label: t('ms.apiTestDebug.newApi'), label: t('ms.apiTestDebug.newApi'),
closable: true, closable: true,
method: RequestMethods.GET, method: RequestMethods.GET,
unSave: true, unSave: false,
params: [], headerParams: [],
bodyParams: cloneDeep(defaultBodyParams),
}, },
]); ]);
const debugUrl = ref(''); const debugUrl = ref('');
@ -133,15 +172,22 @@
activeDebug.value = item; activeDebug.value = item;
} }
function handleActiveDebugChange() {
activeDebug.value.unSave = true;
}
function addDebugTab() { function addDebugTab() {
const id = `debug-${Date.now()}`; const id = `debug-${Date.now()}`;
debugTabs.value.push({ debugTabs.value.push({
id, id,
moduleProtocol: 'http',
activeTab: RequestComposition.HEADER,
label: t('ms.apiTestDebug.newApi'), label: t('ms.apiTestDebug.newApi'),
closable: true, closable: true,
method: RequestMethods.GET, method: RequestMethods.GET,
unSave: true, unSave: false,
params: [], headerParams: [],
bodyParams: cloneDeep(defaultBodyParams),
}); });
activeTab.value = id; activeTab.value = id;
} }
@ -165,7 +211,6 @@
}, },
]; ];
const contentTab = ref(RequestComposition.HEADER);
const contentTabList = [ const contentTabList = [
{ {
value: RequestComposition.HEADER, value: RequestComposition.HEADER,
@ -205,6 +250,13 @@
}, },
]; ];
const moduleProtocolOptions = ref([
{
label: 'HTTP',
value: 'http',
},
]);
const splitBoxSize = ref<string | number>(0.6); const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical'); const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>(); const splitContainerRef = ref<HTMLElement>();
@ -241,6 +293,22 @@
isExpanded.value = true; isExpanded.value = true;
splitBoxSize.value = 0.6; splitBoxSize.value = 0.6;
} }
function saveDebug() {
activeDebug.value.unSave = false;
}
onMounted(() => {
registerCatchSaveShortcut(saveDebug);
});
onBeforeUnmount(() => {
removeCatchSaveShortcut(saveDebug);
});
defineExpose({
addDebugTab,
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,12 +1,18 @@
<template> <template>
<div> <div>
<div class="mb-[8px] flex items-center gap-[8px]"> <div class="mb-[8px] flex items-center gap-[8px]">
<a-select v-model:model-value="moduleProtocol" :options="moduleProtocolOptions" class="w-[90px]"></a-select>
<a-input <a-input
v-model:model-value="moduleKeyword" v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')" :placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear allow-clear
/> />
<a-dropdown @select="handleSelect">
<a-button type="primary">{{ t('ms.apiTestDebug.newApi') }}</a-button>
<template #content>
<a-doption value="newApi">{{ t('ms.apiTestDebug.newApi') }}</a-doption>
<a-doption value="import">{{ t('ms.apiTestDebug.importApi') }}</a-doption>
</template>
</a-dropdown>
</div> </div>
<div v-if="!props.isModal" class="folder"> <div v-if="!props.isModal" class="folder">
<div class="folder-text"> <div class="folder-text">
@ -118,12 +124,25 @@
modulesCount?: Record<string, number>; // modulesCount?: Record<string, number>; //
isExpandAll?: boolean; // isExpandAll?: boolean; //
}>(); }>();
const emit = defineEmits(['init', 'folderNodeSelect']); const emit = defineEmits(['init', 'folderNodeSelect', 'newApi']);
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'newApi':
emit('newApi');
break;
case 'import':
break;
default:
break;
}
}
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
if (props.isModal) { if (props.isModal) {
return { return {
@ -151,13 +170,6 @@
isExpandAll.value = !isExpandAll.value; isExpandAll.value = !isExpandAll.value;
} }
const moduleProtocol = ref('http');
const moduleProtocolOptions = ref([
{
label: 'HTTP',
value: 'http',
},
]);
const moduleKeyword = ref(''); const moduleKeyword = ref('');
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
const focusNodeKey = ref<string | number>(''); const focusNodeKey = ref<string | number>('');

View File

@ -2,17 +2,13 @@
<MsCard simple no-content-padding> <MsCard simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5"> <MsSplitBox :size="0.25" :max="0.5">
<template #first> <template #first>
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]"> <div class="p-[24px]">
<a-button type="primary" class="mr-[12px]">{{ t('ms.apiTestDebug.createDebug') }}</a-button> <moduleTree @new-api="newApi" />
<a-button type="outline">{{ t('common.import') }}</a-button>
</div>
<div class="px-[24px] py-[16px]">
<moduleTree />
</div> </div>
</template> </template>
<template #second> <template #second>
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<debug /> <debug ref="debugRef" />
</div> </div>
</template> </template>
</MsSplitBox> </MsSplitBox>
@ -25,22 +21,11 @@
import debug from './components/debug/index.vue'; import debug from './components/debug/index.vue';
import moduleTree from './components/moduleTree.vue'; import moduleTree from './components/moduleTree.vue';
import { useI18n } from '@/hooks/useI18n'; const debugRef = ref<InstanceType<typeof debug>>();
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
const { t } = useI18n(); function newApi() {
debugRef.value?.addDebugTab();
function saveDebug() {
console.log('save');
} }
onMounted(() => {
registerCatchSaveShortcut(saveDebug);
});
onBeforeUnmount(() => {
removeCatchSaveShortcut(saveDebug);
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -1,6 +1,6 @@
export default { export default {
'ms.apiTestDebug.createDebug': 'New debug',
'ms.apiTestDebug.newApi': 'New request', 'ms.apiTestDebug.newApi': 'New request',
'ms.apiTestDebug.importApi': 'Import request',
'ms.apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https', 'ms.apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https',
'ms.apiTestDebug.serverExec': 'Server execution', 'ms.apiTestDebug.serverExec': 'Server execution',
'ms.apiTestDebug.localExec': 'Local execution', 'ms.apiTestDebug.localExec': 'Local execution',

View File

@ -1,6 +1,6 @@
export default { export default {
'ms.apiTestDebug.createDebug': '新建调试',
'ms.apiTestDebug.newApi': '新建请求', 'ms.apiTestDebug.newApi': '新建请求',
'ms.apiTestDebug.importApi': '导入请求',
'ms.apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL', 'ms.apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL',
'ms.apiTestDebug.serverExec': '服务端执行', 'ms.apiTestDebug.serverExec': '服务端执行',
'ms.apiTestDebug.localExec': '本地执行', 'ms.apiTestDebug.localExec': '本地执行',
@ -18,13 +18,23 @@ export default {
'ms.apiTestDebug.horizontal': '左右布局', 'ms.apiTestDebug.horizontal': '左右布局',
'ms.apiTestDebug.paramName': '参数名称', 'ms.apiTestDebug.paramName': '参数名称',
'ms.apiTestDebug.paramNamePlaceholder': '请输入参数名称', 'ms.apiTestDebug.paramNamePlaceholder': '请输入参数名称',
'ms.apiTestDebug.paramRequired': '必填',
'ms.apiTestDebug.paramNotRequired': '非必填',
'ms.apiTestDebug.paramType': '类型',
'ms.apiTestDebug.paramValue': '参数值', 'ms.apiTestDebug.paramValue': '参数值',
'ms.apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入', 'ms.apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入',
'ms.apiTestDebug.paramLengthRange': '长度区间',
'ms.apiTestDebug.paramMin': '最小值',
'ms.apiTestDebug.paramMax': '最大值',
'ms.apiTestDebug.paramValuePreview': '参数预览', 'ms.apiTestDebug.paramValuePreview': '参数预览',
'ms.apiTestDebug.desc': '描述', 'ms.apiTestDebug.desc': '描述',
'ms.apiTestDebug.encode': '编码',
'ms.apiTestDebug.encodeTip1': '开启:使用编码',
'ms.apiTestDebug.encodeTip2': '关闭:不使用编码',
'ms.apiTestDebug.apply': '应用', 'ms.apiTestDebug.apply': '应用',
'ms.apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural', 'ms.apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural',
'ms.apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据', 'ms.apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据',
'ms.apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等', 'ms.apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等',
'ms.apiTestDebug.descPlaceholder': '请输入内容', 'ms.apiTestDebug.descPlaceholder': '请输入内容',
'ms.apiTestDebug.noneBody': '请求没有 Body',
}; };