feat(全局): 接口调试-响应内容&部分组件调整
This commit is contained in:
parent
7e5967a688
commit
31562ac774
|
@ -1,4 +1,5 @@
|
||||||
/** 主题变量覆盖 **/
|
/** 主题变量覆盖 **/
|
||||||
|
@border-radius-mini: 2px;
|
||||||
@border-radius-small: 4px;
|
@border-radius-small: 4px;
|
||||||
@border-radius-medium: 6px;
|
@border-radius-medium: 6px;
|
||||||
@border-radius-large: 12px;
|
@border-radius-large: 12px;
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="fullRef" class="h-full rounded-[4px] bg-[var(--color-fill-1)] p-[12px]">
|
<div ref="fullRef" class="h-full rounded-[4px] bg-[var(--color-fill-1)] p-[12px]">
|
||||||
<div v-if="showTitleLine" class="mb-[12px] flex items-center justify-between pr-[12px]">
|
<div v-if="showTitleLine" class="mb-[12px] flex items-center justify-between">
|
||||||
<div>
|
<div class="flex flex-wrap gap-[4px]">
|
||||||
<a-select
|
<a-select
|
||||||
v-if="showLanguageChange"
|
v-if="showLanguageChange"
|
||||||
v-model:model-value="currentLanguage"
|
v-model:model-value="currentLanguage"
|
||||||
:options="languageOptions"
|
:options="languageOptions"
|
||||||
class="mr-[4px] w-[100px]"
|
class="w-[100px]"
|
||||||
size="small"
|
size="small"
|
||||||
@change="(val) => handleLanguageChange(val as Language)"
|
@change="(val) => handleLanguageChange(val as Language)"
|
||||||
/>
|
/>
|
||||||
|
<a-select
|
||||||
|
v-if="showCharsetChange"
|
||||||
|
v-model:model-value="currentCharset"
|
||||||
|
:options="charsetOptions"
|
||||||
|
class="w-[100px]"
|
||||||
|
size="small"
|
||||||
|
@change="(val) => handleCharsetChange(val as string)"
|
||||||
|
/>
|
||||||
<a-select
|
<a-select
|
||||||
v-if="showThemeChange"
|
v-if="showThemeChange"
|
||||||
v-model:model-value="currentTheme"
|
v-model:model-value="currentTheme"
|
||||||
|
@ -34,9 +42,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 这里的 32px 是顶部标题的 32px -->
|
<!-- 这里的 40px 是顶部标题的 40px -->
|
||||||
<div :class="`flex ${showTitleLine ? 'h-[calc(100%-32px)]' : 'h-full'} w-full flex-row`">
|
<div :class="`flex ${showTitleLine ? 'h-[calc(100%-40px)]' : 'h-full'} w-full flex-row`">
|
||||||
<div ref="codeEditBox" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
|
<div ref="codeContainerRef" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
|
||||||
<slot name="rightBox"> </slot>
|
<slot name="rightBox"> </slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +54,9 @@
|
||||||
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useFullscreen } from '@vueuse/core';
|
import { useFullscreen } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { codeCharset } from '@/config/apiTest';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { decodeStringToCharset } from '@/utils';
|
||||||
|
|
||||||
import './userWorker';
|
import './userWorker';
|
||||||
import MsCodeEditorTheme from './themes';
|
import MsCodeEditorTheme from './themes';
|
||||||
|
@ -59,9 +69,34 @@
|
||||||
emits: ['update:modelValue', 'change'],
|
emits: ['update:modelValue', 'change'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
// 编辑器实例,每次调用组件都会创建独立的实例
|
||||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
const codeEditBox = ref();
|
const codeContainerRef = ref();
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
// 注册自定义主题
|
||||||
|
Object.keys(MsCodeEditorTheme).forEach((e) => {
|
||||||
|
monaco.editor.defineTheme(e, MsCodeEditorTheme[e as CustomTheme]);
|
||||||
|
});
|
||||||
|
editor = monaco.editor.create(codeContainerRef.value, {
|
||||||
|
value: props.modelValue,
|
||||||
|
automaticLayout: true,
|
||||||
|
padding: {
|
||||||
|
top: 12,
|
||||||
|
bottom: 12,
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听值的变化
|
||||||
|
editor.onDidBlurEditorText(() => {
|
||||||
|
const value = editor.getValue(); // 给父组件实时返回最新文本
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
emit('change', value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 用于全屏的容器 ref
|
// 用于全屏的容器 ref
|
||||||
const fullRef = ref<HTMLElement | null>();
|
const fullRef = ref<HTMLElement | null>();
|
||||||
// 当前主题
|
// 当前主题
|
||||||
|
@ -77,6 +112,12 @@
|
||||||
value: item,
|
value: item,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
function handleThemeChange(val: Theme) {
|
||||||
|
editor.updateOptions({
|
||||||
|
theme: val,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 当前语言
|
// 当前语言
|
||||||
const currentLanguage = ref<Language>(props.language);
|
const currentLanguage = ref<Language>(props.language);
|
||||||
// 语言选项
|
// 语言选项
|
||||||
|
@ -98,10 +139,29 @@
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean) as { label: string; value: Language }[];
|
.filter(Boolean) as { label: string; value: Language }[];
|
||||||
|
function handleLanguageChange(val: Language) {
|
||||||
|
monaco.editor.setModelLanguage(editor.getModel()!, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前字符集
|
||||||
|
const currentCharset = ref('UTF-8');
|
||||||
|
// 字符集选项
|
||||||
|
const charsetOptions = codeCharset.map((e) => ({
|
||||||
|
label: e,
|
||||||
|
value: e,
|
||||||
|
}));
|
||||||
|
function handleCharsetChange(val: string) {
|
||||||
|
editor.setValue(decodeStringToCharset(props.modelValue, val));
|
||||||
|
}
|
||||||
|
|
||||||
// 是否显示标题栏
|
// 是否显示标题栏
|
||||||
const showTitleLine = computed(
|
const showTitleLine = computed(
|
||||||
() => props.title || props.showThemeChange || props.showLanguageChange || props.showFullScreen
|
() =>
|
||||||
|
props.title ||
|
||||||
|
props.showThemeChange ||
|
||||||
|
props.showLanguageChange ||
|
||||||
|
props.showCharsetChange ||
|
||||||
|
props.showFullScreen
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -111,33 +171,6 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleThemeChange(val: Theme) {
|
|
||||||
monaco.editor.setTheme(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLanguageChange(val: Language) {
|
|
||||||
monaco.editor.setModelLanguage(editor.getModel()!, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
// 注册自定义主题
|
|
||||||
Object.keys(MsCodeEditorTheme).forEach((e) => {
|
|
||||||
monaco.editor.defineTheme(e, MsCodeEditorTheme[e as CustomTheme]);
|
|
||||||
});
|
|
||||||
editor = monaco.editor.create(codeEditBox.value, {
|
|
||||||
value: props.modelValue,
|
|
||||||
automaticLayout: true,
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听值的变化
|
|
||||||
editor.onDidBlurEditorText(() => {
|
|
||||||
const value = editor.getValue(); // 给父组件实时返回最新文本
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
emit('change', value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setEditBoxBg = () => {
|
const setEditBoxBg = () => {
|
||||||
const codeBgEl = document.querySelector('.monaco-editor-background');
|
const codeBgEl = document.querySelector('.monaco-editor-background');
|
||||||
if (codeBgEl) {
|
if (codeBgEl) {
|
||||||
|
@ -146,7 +179,7 @@
|
||||||
|
|
||||||
// 获取背景颜色
|
// 获取背景颜色
|
||||||
const { backgroundColor } = computedStyle;
|
const { backgroundColor } = computedStyle;
|
||||||
codeEditBox.value.style.backgroundColor = backgroundColor;
|
codeContainerRef.value.style.backgroundColor = backgroundColor;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -222,18 +255,21 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
codeEditBox,
|
codeContainerRef,
|
||||||
fullRef,
|
fullRef,
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
themeOptions,
|
themeOptions,
|
||||||
currentLanguage,
|
currentLanguage,
|
||||||
languageOptions,
|
languageOptions,
|
||||||
|
currentCharset,
|
||||||
|
charsetOptions,
|
||||||
showTitleLine,
|
showTitleLine,
|
||||||
toggle,
|
toggle,
|
||||||
t,
|
t,
|
||||||
handleThemeChange,
|
handleThemeChange,
|
||||||
handleLanguageChange,
|
handleLanguageChange,
|
||||||
|
handleCharsetChange,
|
||||||
insertContent,
|
insertContent,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
|
@ -244,11 +280,11 @@
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.ms-code-editor {
|
.ms-code-editor {
|
||||||
@apply z-10;
|
@apply z-10 overflow-hidden;
|
||||||
|
|
||||||
padding: 16px 0;
|
|
||||||
width: v-bind(width);
|
width: v-bind(width);
|
||||||
height: v-bind(height);
|
height: v-bind(height);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
&[data-mode-id='plaintext'] {
|
&[data-mode-id='plaintext'] {
|
||||||
:deep(.mtk1) {
|
:deep(.mtk1) {
|
||||||
color: rgb(var(--primary-5));
|
color: rgb(var(--primary-5));
|
||||||
|
|
|
@ -94,10 +94,17 @@ export const editorProps = {
|
||||||
type: Array as PropType<Array<Language>>,
|
type: Array as PropType<Array<Language>>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
// 是否代码语言切换
|
||||||
showLanguageChange: {
|
showLanguageChange: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<boolean>,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
// 是否显示字符集切换
|
||||||
|
showCharsetChange: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 是否显示主题切换
|
||||||
showThemeChange: {
|
showThemeChange: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<boolean>,
|
||||||
default: true,
|
default: true,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tab-container">
|
<div class="ms-editable-tab-container">
|
||||||
<a-tooltip v-if="!isNotOverflow" :content="t('ms.editableTab.arrivedLeft')" :disabled="!arrivedState.left">
|
<a-tooltip v-if="!isNotOverflow" :content="t('ms.editableTab.arrivedLeft')" :disabled="!arrivedState.left">
|
||||||
<MsButton
|
<MsButton
|
||||||
type="icon"
|
type="icon"
|
||||||
|
@ -11,21 +11,22 @@
|
||||||
<MsIcon type="icon-icon_left_outlined" />
|
<MsIcon type="icon-icon_left_outlined" />
|
||||||
</MsButton>
|
</MsButton>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<div ref="tabNav" class="tab-nav">
|
<div ref="tabNav" class="ms-editable-tab-nav">
|
||||||
<div
|
<div
|
||||||
v-for="tab in props.tabs"
|
v-for="tab in props.tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
class="tab"
|
class="ms-editable-tab"
|
||||||
:class="{ active: innerActiveTab === tab.id }"
|
:class="{ active: innerActiveTab === tab.id }"
|
||||||
@click="handleTabClick(tab)"
|
@click="handleTabClick(tab)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<slot name="label" :tab="tab">{{ tab.label }}</slot>
|
<slot name="label" :tab="tab">{{ tab.label }}</slot>
|
||||||
|
<div v-if="tab.unSaved" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
|
||||||
<MsButton
|
<MsButton
|
||||||
v-if="tab.closable"
|
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable"
|
||||||
type="icon"
|
type="icon"
|
||||||
status="secondary"
|
status="secondary"
|
||||||
class="tab-close-button"
|
class="ms-editable-tab-close-button"
|
||||||
@click="() => close(tab)"
|
@click="() => close(tab)"
|
||||||
>
|
>
|
||||||
<MsIcon type="icon-icon_close_outlined" size="12" />
|
<MsIcon type="icon-icon_close_outlined" size="12" />
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
<MsButton
|
<MsButton
|
||||||
type="icon"
|
type="icon"
|
||||||
status="secondary"
|
status="secondary"
|
||||||
class="tab-button !mr-[8px]"
|
class="ms-editable-tab-button !mr-[8px]"
|
||||||
:disabled="arrivedState.right"
|
:disabled="arrivedState.right"
|
||||||
@click="scrollTabs('right')"
|
@click="scrollTabs('right')"
|
||||||
>
|
>
|
||||||
|
@ -51,15 +52,19 @@
|
||||||
<MsButton
|
<MsButton
|
||||||
type="icon"
|
type="icon"
|
||||||
status="secondary"
|
status="secondary"
|
||||||
class="tab-button !mr-[4px]"
|
class="ms-editable-tab-button !mr-[4px]"
|
||||||
:disabled="!!props.limit && props.tabs.length >= props.limit"
|
:disabled="!!props.limit && props.tabs.length >= props.limit"
|
||||||
@click="addTab"
|
@click="addTab"
|
||||||
>
|
>
|
||||||
<MsIcon type="icon-icon_add_outlined" />
|
<MsIcon type="icon-icon_add_outlined" />
|
||||||
</MsButton>
|
</MsButton>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<MsMoreAction v-if="props.moreActionList" :list="props.moreActionList">
|
<MsMoreAction
|
||||||
<MsButton type="icon" status="secondary" class="tab-button">
|
v-if="props.moreActionList"
|
||||||
|
:list="props.moreActionList"
|
||||||
|
@select="(val) => emit('moreActionSelect', val)"
|
||||||
|
>
|
||||||
|
<MsButton type="icon" status="secondary" class="ms-editable-tab-button">
|
||||||
<MsIcon type="icon-icon_more_outlined" />
|
<MsIcon type="icon-icon_more_outlined" />
|
||||||
</MsButton>
|
</MsButton>
|
||||||
</MsMoreAction>
|
</MsMoreAction>
|
||||||
|
@ -69,6 +74,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { useScroll, useVModel } from '@vueuse/core';
|
import { useScroll, useVModel } from '@vueuse/core';
|
||||||
|
import { useDraggable } from 'vue-draggable-plus';
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
@ -84,17 +90,21 @@
|
||||||
activeTab: string | number;
|
activeTab: string | number;
|
||||||
moreActionList?: ActionsItem[];
|
moreActionList?: ActionsItem[];
|
||||||
limit?: number; // 最多可打开的tab数量
|
limit?: number; // 最多可打开的tab数量
|
||||||
|
atLeastOne?: boolean; // 是否至少保留一个tab
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:tabs', activeTab: string | number): void;
|
||||||
(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: 'change', item: TabItem): void;
|
(e: 'change', item: TabItem): void;
|
||||||
|
(e: 'moreActionSelect', item: ActionsItem): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerActiveTab = useVModel(props, 'activeTab', emit);
|
const innerActiveTab = useVModel(props, 'activeTab', emit);
|
||||||
|
const innerTabs = useVModel(props, 'tabs', emit);
|
||||||
const tabNav = ref<HTMLElement | null>(null);
|
const tabNav = ref<HTMLElement | null>(null);
|
||||||
const { arrivedState } = useScroll(tabNav);
|
const { arrivedState } = useScroll(tabNav);
|
||||||
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
||||||
|
@ -139,6 +149,9 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(props.tabs, () => {
|
watch(props.tabs, () => {
|
||||||
|
useDraggable('.ms-editable-tab-nav', innerTabs, {
|
||||||
|
ghostClass: 'ms-editable-tab-ghost',
|
||||||
|
});
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
scrollToActiveTab();
|
scrollToActiveTab();
|
||||||
});
|
});
|
||||||
|
@ -169,17 +182,17 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.tab-container {
|
.ms-editable-tab-container {
|
||||||
@apply flex items-center;
|
@apply flex items-center;
|
||||||
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
.tab-nav {
|
.ms-editable-tab-nav {
|
||||||
@apply relative flex overflow-x-auto whitespace-nowrap;
|
@apply relative flex overflow-x-auto whitespace-nowrap;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0; /* 宽度为0,隐藏垂直滚动条 */
|
width: 0; /* 宽度为0,隐藏垂直滚动条 */
|
||||||
height: 0; /* 高度为0,隐藏水平滚动条 */
|
height: 0; /* 高度为0,隐藏水平滚动条 */
|
||||||
}
|
}
|
||||||
.tab {
|
.ms-editable-tab {
|
||||||
@apply flex cursor-pointer items-center;
|
@apply flex cursor-pointer items-center;
|
||||||
|
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
@ -191,18 +204,18 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgb(var(--primary-5));
|
color: rgb(var(--primary-5));
|
||||||
background-color: rgb(var(--primary-1));
|
background-color: rgb(var(--primary-1));
|
||||||
.tab-close-button {
|
.ms-editable-tab-close-button {
|
||||||
@apply visible;
|
@apply visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tab-close-button {
|
.ms-editable-tab-close-button {
|
||||||
@apply invisible !rounded-full;
|
@apply invisible !rounded-full;
|
||||||
|
|
||||||
margin-left: 4px !important;
|
margin-left: 4px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tab-button {
|
.ms-editable-tab-button {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
&:not([disabled='true']) {
|
&:not([disabled='true']) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -213,4 +226,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ms-editable-tab-ghost {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,5 +2,6 @@ export interface TabItem {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
closable?: boolean;
|
closable?: boolean;
|
||||||
|
unSaved?: boolean; // 未保存
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
'ms.jsonpathPicker.xmlNotValid': '非法的XML文本',
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
'ms.jsonpathPicker.xmlNotValid': '非法的XML文本',
|
||||||
|
};
|
|
@ -1,24 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="parsedXml" class="container">
|
<div>
|
||||||
<div v-for="(node, index) in flattenedXml" :key="index">
|
<div v-if="parsedXml" class="container">
|
||||||
<span style="white-space: pre" @click="copyXPath(node.xpath)" v-html="node.content"></span>
|
<div v-for="(node, index) in flattenedXml" :key="index">
|
||||||
|
<span style="white-space: pre" @click="copyXPath(node.xpath)" v-html="node.content"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!isValidXml">{{ t('ms.jsonpathPicker.xmlNotValid') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
import { XpathNode } from './types';
|
import { XpathNode } from './types';
|
||||||
import * as XmlBeautify from 'xml-beautify';
|
import * as XmlBeautify from 'xml-beautify';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
xmlString: string;
|
xmlString: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['pick']);
|
const emit = defineEmits(['pick']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const parsedXml = ref<Document | null>(null);
|
const parsedXml = ref<Document | null>(null);
|
||||||
const flattenedXml = ref<XpathNode[]>([]);
|
const flattenedXml = ref<XpathNode[]>([]);
|
||||||
const tempXmls = ref<XpathNode[]>([]);
|
const tempXmls = ref<XpathNode[]>([]);
|
||||||
|
const isValidXml = ref(true); // 是否是合法的xml
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取同名兄弟节点
|
* 获取同名兄弟节点
|
||||||
|
@ -68,6 +75,7 @@
|
||||||
emit('pick', xpath);
|
emit('pick', xpath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析xml
|
* 解析xml
|
||||||
*/
|
*/
|
||||||
|
@ -75,6 +83,12 @@
|
||||||
try {
|
try {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
|
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
|
||||||
|
// 如果存在 parsererror 元素,说明 XML 不合法
|
||||||
|
const errors = xmlDoc.getElementsByTagName('parsererror');
|
||||||
|
if (errors.length > 0) {
|
||||||
|
isValidXml.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
parsedXml.value = xmlDoc;
|
parsedXml.value = xmlDoc;
|
||||||
// 先将 XML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
// 先将 XML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
||||||
flattenedXml.value = new XmlBeautify({ parser: DOMParser })
|
flattenedXml.value = new XmlBeautify({ parser: DOMParser })
|
||||||
|
@ -88,7 +102,7 @@
|
||||||
flattenXml(xmlDoc.documentElement, '');
|
flattenXml(xmlDoc.documentElement, '');
|
||||||
// 将扁平化后的 XML 字符串中的每个节点的 xpath 替换为真实的 xpath
|
// 将扁平化后的 XML 字符串中的每个节点的 xpath 替换为真实的 xpath
|
||||||
flattenedXml.value = flattenedXml.value.map((e) => {
|
flattenedXml.value = flattenedXml.value.map((e) => {
|
||||||
const targetNodeIndex = tempXmls.value.findIndex((t) => e.content.includes(`<${t.content}`));
|
const targetNodeIndex = tempXmls.value.findIndex((txt) => e.content.includes(`<${txt.content}`));
|
||||||
if (targetNodeIndex >= 0) {
|
if (targetNodeIndex >= 0) {
|
||||||
const { xpath } = tempXmls.value[targetNodeIndex];
|
const { xpath } = tempXmls.value[targetNodeIndex];
|
||||||
tempXmls.value.splice(targetNodeIndex, 1); // 匹配成功后,将匹配到的节点从 tempXmls 中删除,避免重复匹配
|
tempXmls.value.splice(targetNodeIndex, 1); // 匹配成功后,将匹配到的节点从 tempXmls 中删除,避免重复匹配
|
||||||
|
|
|
@ -90,6 +90,14 @@ export default {
|
||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
tag: 'Tag',
|
tag: 'Tag',
|
||||||
},
|
},
|
||||||
|
tag: {
|
||||||
|
case: 'Case',
|
||||||
|
module: 'Module',
|
||||||
|
precondition: 'Precondition',
|
||||||
|
desc: 'Step desc',
|
||||||
|
expect: 'Expected result',
|
||||||
|
remark: 'Remark',
|
||||||
|
},
|
||||||
hotboxMenu: {
|
hotboxMenu: {
|
||||||
expand: 'Expand/Collapse',
|
expand: 'Expand/Collapse',
|
||||||
insetParent: 'Insert one level up',
|
insetParent: 'Insert one level up',
|
||||||
|
|
|
@ -84,6 +84,14 @@ export default {
|
||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
tag: '标签',
|
tag: '标签',
|
||||||
},
|
},
|
||||||
|
tag: {
|
||||||
|
case: '用例',
|
||||||
|
module: '模块',
|
||||||
|
precondition: '前置条件',
|
||||||
|
desc: '步骤描述',
|
||||||
|
expect: '预期结果',
|
||||||
|
remark: '备注',
|
||||||
|
},
|
||||||
hotboxMenu: {
|
hotboxMenu: {
|
||||||
expand: '展开/收起',
|
expand: '展开/收起',
|
||||||
insetParent: '插入上一级',
|
insetParent: '插入上一级',
|
||||||
|
|
|
@ -67,12 +67,14 @@
|
||||||
|
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
|
||||||
|
export type Direction = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
size?: number | string; // 左侧宽度/顶部容器高度。expandDirection为 right 时,size 也是左侧容器宽度,所以想要缩小右侧容器宽度只需要将 size 调大即可
|
size?: number | string; // 左侧宽度/顶部容器高度。expandDirection为 right 时,size 也是左侧容器宽度,所以想要缩小右侧容器宽度只需要将 size 调大即可
|
||||||
min?: number | string;
|
min?: number | string;
|
||||||
max?: number | string;
|
max?: number | string;
|
||||||
direction?: 'horizontal' | 'vertical';
|
direction?: Direction;
|
||||||
expandDirection?: 'left' | 'right' | 'top'; // TODO: 未实现 bottom,有场景再补充。目前默认水平是 left,垂直是 top
|
expandDirection?: 'left' | 'right' | 'top'; // TODO: 未实现 bottom,有场景再补充。目前默认水平是 left,垂直是 top
|
||||||
disabled?: boolean; // 是否禁用
|
disabled?: boolean; // 是否禁用
|
||||||
firstContainerClass?: string; // first容器类名
|
firstContainerClass?: string; // first容器类名
|
||||||
|
@ -214,7 +216,9 @@
|
||||||
@apply h-full bg-white;
|
@apply h-full bg-white;
|
||||||
}
|
}
|
||||||
.vertical-expand-line {
|
.vertical-expand-line {
|
||||||
@apply relative z-10 flex items-center justify-center bg-transparent;
|
@apply relative flex items-center justify-center bg-transparent;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
&::before {
|
&::before {
|
||||||
@apply absolute w-full bg-transparent;
|
@apply absolute w-full bg-transparent;
|
||||||
|
|
||||||
|
|
|
@ -8,5 +8,5 @@ export const conditionTypeNameMap = {
|
||||||
waitTime: 'apiTestDebug.waitTime',
|
waitTime: 'apiTestDebug.waitTime',
|
||||||
extract: 'apiTestDebug.extractParameter',
|
extract: 'apiTestDebug.extractParameter',
|
||||||
};
|
};
|
||||||
|
// 代码字符集
|
||||||
export default {};
|
export const codeCharset = ['UTF-8', 'UTF-16', 'GBK', 'GB2312', 'ISO-8859-1', 'Shift_JIS', 'ASCII', 'BIG5', 'KOI8-R'];
|
||||||
|
|
|
@ -95,3 +95,12 @@ export const reviewDefaultDetail: ReviewItem = {
|
||||||
reReviewedCount: 0,
|
reReviewedCount: 0,
|
||||||
followFlag: false,
|
followFlag: false,
|
||||||
};
|
};
|
||||||
|
// 脑图-标签
|
||||||
|
export const minderTagMap = {
|
||||||
|
CASE: 'minder.tag.case',
|
||||||
|
MODULE: 'minder.tag.module',
|
||||||
|
PREREQUISITE: 'minder.tag.precondition',
|
||||||
|
TEXT_DESCRIPTION: 'minder.tag.desc',
|
||||||
|
EXPECTED_RESULT: 'minder.tag.expect',
|
||||||
|
DESCRIPTION: 'minder.tag.remark',
|
||||||
|
};
|
||||||
|
|
|
@ -43,3 +43,12 @@ export enum RequestContentTypeEnum {
|
||||||
ATOM_XML = 'application/atom+xml',
|
ATOM_XML = 'application/atom+xml',
|
||||||
ECMASCRIPT = 'application/ecmascript',
|
ECMASCRIPT = 'application/ecmascript',
|
||||||
}
|
}
|
||||||
|
// 接口响应组成部分
|
||||||
|
export enum ResponseComposition {
|
||||||
|
BODY = 'BODY',
|
||||||
|
HEADER = 'HEADER',
|
||||||
|
REAL_REQUEST = 'REAL_REQUEST', // 实际请求
|
||||||
|
CONSOLE = 'CONSOLE',
|
||||||
|
EXTRACT = 'EXTRACT',
|
||||||
|
ASSERTION = 'ASSERTION',
|
||||||
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ export default {
|
||||||
'common.collapseAll': 'Collapse all',
|
'common.collapseAll': 'Collapse all',
|
||||||
'common.expandAll': 'Expand all',
|
'common.expandAll': 'Expand all',
|
||||||
'common.copy': 'Copy',
|
'common.copy': 'Copy',
|
||||||
|
'common.copySuccess': 'Copy successfully',
|
||||||
'common.fork': 'Fork',
|
'common.fork': 'Fork',
|
||||||
'common.forked': 'Forked',
|
'common.forked': 'Forked',
|
||||||
'common.more': 'More',
|
'common.more': 'More',
|
||||||
|
@ -95,4 +96,6 @@ export default {
|
||||||
'common.revoke': 'Revoke',
|
'common.revoke': 'Revoke',
|
||||||
'common.clear': 'Clear',
|
'common.clear': 'Clear',
|
||||||
'common.tag': 'Tag',
|
'common.tag': 'Tag',
|
||||||
|
'common.success': 'Success',
|
||||||
|
'common.fail': 'Failed',
|
||||||
};
|
};
|
||||||
|
|
|
@ -75,6 +75,7 @@ export default {
|
||||||
'common.collapseAll': '收起全部',
|
'common.collapseAll': '收起全部',
|
||||||
'common.expandAll': '展开全部',
|
'common.expandAll': '展开全部',
|
||||||
'common.copy': '复制',
|
'common.copy': '复制',
|
||||||
|
'common.copySuccess': '复制成功',
|
||||||
'common.fork': '关注',
|
'common.fork': '关注',
|
||||||
'common.forked': '已关注',
|
'common.forked': '已关注',
|
||||||
'common.more': '更多',
|
'common.more': '更多',
|
||||||
|
@ -98,4 +99,6 @@ export default {
|
||||||
'common.revoke': '撤销',
|
'common.revoke': '撤销',
|
||||||
'common.clear': '清空',
|
'common.clear': '清空',
|
||||||
'common.tag': '标签',
|
'common.tag': '标签',
|
||||||
|
'common.success': '成功',
|
||||||
|
'common.fail': '失败',
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,3 +11,15 @@ export interface ExpressionConfig {
|
||||||
specifyMatchNum?: number; // 指定匹配下标
|
specifyMatchNum?: number; // 指定匹配下标
|
||||||
xmlMatchContentType?: 'xml' | 'html'; // 响应内容格式
|
xmlMatchContentType?: 'xml' | 'html'; // 响应内容格式
|
||||||
}
|
}
|
||||||
|
// 响应时间信息
|
||||||
|
export interface ResponseTiming {
|
||||||
|
ready: number;
|
||||||
|
socketInit: number;
|
||||||
|
dnsQuery: number;
|
||||||
|
tcpHandshake: number;
|
||||||
|
sslHandshake: number;
|
||||||
|
waitingTTFB: number;
|
||||||
|
downloadContent: number;
|
||||||
|
deal: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import JSEncrypt from 'jsencrypt';
|
import JSEncrypt from 'jsencrypt';
|
||||||
|
|
||||||
|
import { codeCharset } from '@/config/apiTest';
|
||||||
|
|
||||||
import { isObject } from './is';
|
import { isObject } from './is';
|
||||||
|
|
||||||
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
||||||
|
@ -324,7 +326,7 @@ export const getHashParameters = (): Record<string, string> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 id 序列号
|
* 生成 id 序列号
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getGenerateId = () => {
|
export const getGenerateId = () => {
|
||||||
|
@ -357,3 +359,14 @@ export const downloadByteFile = (byte: BlobPart, fileName: string) => {
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换字符串的字符集编码
|
||||||
|
* @param str 需要转换的字符串
|
||||||
|
* @param charset 字符集编码
|
||||||
|
*/
|
||||||
|
export function decodeStringToCharset(str: string, charset = 'UTF-8') {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder(charset);
|
||||||
|
return decoder.decode(encoder.encode(str));
|
||||||
|
}
|
||||||
|
|
|
@ -655,6 +655,9 @@ org.apache.http.client.method . . . '' at line number 2
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
:deep(.arco-table-th) {
|
||||||
|
background-color: var(--color-text-n9);
|
||||||
|
}
|
||||||
.condition-content {
|
.condition-content {
|
||||||
@apply flex-1 overflow-y-auto;
|
@apply flex-1 overflow-y-auto;
|
||||||
.ms-scroll-bar();
|
.ms-scroll-bar();
|
||||||
|
|
|
@ -257,7 +257,7 @@
|
||||||
}
|
}
|
||||||
.code-container {
|
.code-container {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
height: 400px;
|
max-height: 400px;
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
background-color: var(--color-text-n9);
|
background-color: var(--color-text-n9);
|
||||||
}
|
}
|
||||||
|
|
|
@ -567,7 +567,7 @@
|
||||||
background-color: var(--color-text-n9);
|
background-color: var(--color-text-n9);
|
||||||
}
|
}
|
||||||
:deep(.arco-table-cell-align-left) {
|
:deep(.arco-table-cell-align-left) {
|
||||||
padding: 16px 12px;
|
padding: 16px 2px;
|
||||||
}
|
}
|
||||||
:deep(.arco-table-td) {
|
:deep(.arco-table-td) {
|
||||||
.arco-table-cell {
|
.arco-table-cell {
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex w-full gap-[8px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
|
||||||
|
<div class="text-item-wrapper">
|
||||||
|
<div class="light-item">{{ t('apiTestDebug.responseStage') }}</div>
|
||||||
|
<div class="light-item">{{ t('apiTestDebug.ready') }}</div>
|
||||||
|
<div class="normal-item">{{ t('apiTestDebug.socketInit') }}</div>
|
||||||
|
<div class="normal-item">{{ t('apiTestDebug.dnsQuery') }}</div>
|
||||||
|
<div class="normal-item">{{ t('apiTestDebug.tcpHandshake') }}</div>
|
||||||
|
<div class="normal-item">{{ t('apiTestDebug.sslHandshake') }}</div>
|
||||||
|
<div class="normal-item">{{ t('apiTestDebug.waitingTTFB') }}</div>
|
||||||
|
<div class="normal-item">{{ t('apiTestDebug.downloadContent') }}</div>
|
||||||
|
<div class="light-item">{{ t('apiTestDebug.deal') }}</div>
|
||||||
|
<div class="total-item">{{ t('apiTestDebug.total') }}</div>
|
||||||
|
</div>
|
||||||
|
<a-divider direction="vertical" margin="0" />
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<div class="h-full"></div>
|
||||||
|
<div v-for="line of timingLines" :key="line.key" class="flex h-full items-center bg-transparent">
|
||||||
|
<div
|
||||||
|
class="h-[12px] rounded-[var(--border-radius-mini)] bg-[rgb(var(--success-7))]"
|
||||||
|
:style="{
|
||||||
|
width: line.width,
|
||||||
|
marginLeft: line.left,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full"></div>
|
||||||
|
</div>
|
||||||
|
<a-divider direction="vertical" margin="0" />
|
||||||
|
<div class="text-item-wrapper--right">
|
||||||
|
<div class="light-item">{{ t('apiTestDebug.time') }}</div>
|
||||||
|
<div class="light-item">{{ props.responseTiming.ready }} ms</div>
|
||||||
|
<div class="normal-item">{{ props.responseTiming.socketInit }} ms</div>
|
||||||
|
<div class="normal-item">{{ props.responseTiming.dnsQuery }} ms</div>
|
||||||
|
<div class="normal-item">{{ props.responseTiming.tcpHandshake }} ms</div>
|
||||||
|
<div class="normal-item">{{ props.responseTiming.sslHandshake }} ms</div>
|
||||||
|
<div class="normal-item">{{ props.responseTiming.waitingTTFB }} ms</div>
|
||||||
|
<div class="normal-item">{{ props.responseTiming.downloadContent }} ms</div>
|
||||||
|
<div class="light-item">{{ props.responseTiming.deal }} ms</div>
|
||||||
|
<div class="total-item">{{ props.responseTiming.total }} ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { ResponseTiming } from '@/models/apiTest/debug';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
responseTiming: ResponseTiming;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const timingLines = computed(() => {
|
||||||
|
const arr: { key: string; width: string; left: string }[] = [];
|
||||||
|
const keys = Object.keys(props.responseTiming).filter((key) => key !== 'total');
|
||||||
|
let preLinesTotalLeft = 0;
|
||||||
|
keys.forEach((key, index) => {
|
||||||
|
const itemWidth = (props.responseTiming[key] / props.responseTiming.total) * 100;
|
||||||
|
arr.push({
|
||||||
|
key,
|
||||||
|
width: `${itemWidth}%`,
|
||||||
|
left: index !== 0 ? `${preLinesTotalLeft}%` : '',
|
||||||
|
});
|
||||||
|
preLinesTotalLeft += itemWidth;
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.text-item-wrapper,
|
||||||
|
.text-item-wrapper--right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
.light-item {
|
||||||
|
color: var(--color-text-4);
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.normal-item {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.total-item {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(var(--success-7));
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text-item-wrapper--right {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -28,7 +28,7 @@
|
||||||
v-model:model-value="batchParamsCode"
|
v-model:model-value="batchParamsCode"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
theme="MS-text"
|
theme="MS-text"
|
||||||
height="calc(100% - 12px)"
|
height="100%"
|
||||||
:show-full-screen="false"
|
:show-full-screen="false"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
v-model:model-value="currentBodyCode"
|
v-model:model-value="currentBodyCode"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
height="calc(100% - 12px)"
|
height="100%"
|
||||||
:show-full-screen="false"
|
:show-full-screen="false"
|
||||||
:language="currentCodeLanguage"
|
:language="currentCodeLanguage"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
|
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
|
||||||
<MsEditableTab
|
<MsEditableTab
|
||||||
v-model:active-tab="activeTab"
|
v-model:active-tab="activeRequestTab"
|
||||||
:tabs="debugTabs"
|
v-model:tabs="debugTabs"
|
||||||
:more-action-list="moreActionList"
|
:more-action-list="moreActionList"
|
||||||
|
at-least-one
|
||||||
@add="addDebugTab"
|
@add="addDebugTab"
|
||||||
@close="closeDebugTab"
|
@close="closeDebugTab"
|
||||||
@change="setActiveDebug"
|
@change="setActiveDebug"
|
||||||
|
@more-action-select="handleMoreActionSelect"
|
||||||
>
|
>
|
||||||
<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 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>
|
||||||
|
@ -34,7 +35,7 @@
|
||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:model-value="debugUrl"
|
v-model:model-value="activeDebug.url"
|
||||||
:placeholder="t('apiTestDebug.urlPlaceholder')"
|
:placeholder="t('apiTestDebug.urlPlaceholder')"
|
||||||
@change="handleActiveDebugChange"
|
@change="handleActiveDebugChange"
|
||||||
/>
|
/>
|
||||||
|
@ -129,65 +130,89 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #second>
|
<template #second>
|
||||||
<div class="min-w-[290px] bg-[var(--color-text-n9)] p-[8px_16px]">
|
<response
|
||||||
<div class="flex items-center">
|
v-model:active-layout="activeLayout"
|
||||||
<template v-if="activeLayout === 'vertical'">
|
v-model:active-tab="activeDebug.responseActiveTab"
|
||||||
<MsButton
|
:is-expanded="isExpanded"
|
||||||
v-if="isExpanded"
|
:response="activeDebug.response"
|
||||||
type="icon"
|
@change-expand="changeExpand"
|
||||||
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
@change-layout="handleActiveLayoutChange"
|
||||||
@click="changeExpand(false)"
|
/>
|
||||||
>
|
|
||||||
<icon-down :size="12" />
|
|
||||||
</MsButton>
|
|
||||||
<MsButton v-else type="icon" status="secondary" class="!mr-0 !rounded-full" @click="changeExpand(true)">
|
|
||||||
<icon-right :size="12" />
|
|
||||||
</MsButton>
|
|
||||||
</template>
|
|
||||||
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
|
||||||
<a-radio-group
|
|
||||||
v-model:model-value="activeLayout"
|
|
||||||
type="button"
|
|
||||||
size="small"
|
|
||||||
@change="handleActiveLayoutChange"
|
|
||||||
>
|
|
||||||
<a-radio value="vertical">{{ t('apiTestDebug.vertical') }}</a-radio>
|
|
||||||
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-[16px]"></div>
|
|
||||||
</template>
|
</template>
|
||||||
</MsSplitBox>
|
</MsSplitBox>
|
||||||
</div>
|
</div>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="saveModalVisible"
|
||||||
|
:title="t('common.save')"
|
||||||
|
:ok-loading="saveLoading"
|
||||||
|
class="ms-modal-form"
|
||||||
|
title-align="start"
|
||||||
|
body-class="!p-0"
|
||||||
|
@before-ok="handleSave"
|
||||||
|
>
|
||||||
|
<a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
field="name"
|
||||||
|
:label="t('apiTestDebug.requestName')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="url"
|
||||||
|
:label="t('apiTestDebug.requestUrl')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-input v-model:model-value="saveModalForm.url" :placeholder="t('apiTestDebug.commonPlaceholder')" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
|
||||||
|
<a-tree-select
|
||||||
|
v-model:modelValue="saveModalForm.module"
|
||||||
|
:data="props.moduleTree"
|
||||||
|
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||||
|
allow-search
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||||
import { cloneDeep, debounce } from 'lodash-es';
|
import { cloneDeep, debounce } from 'lodash-es';
|
||||||
|
|
||||||
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 { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import debugAuth from './auth.vue';
|
import debugAuth from './auth.vue';
|
||||||
import debugBody, { BodyParams } from './body.vue';
|
import debugBody, { BodyParams } from './body.vue';
|
||||||
import debugHeader from './header.vue';
|
import debugHeader from './header.vue';
|
||||||
import postcondition from './postcondition.vue';
|
import postcondition from './postcondition.vue';
|
||||||
import precondition from './precondition.vue';
|
import precondition from './precondition.vue';
|
||||||
import debugQuery from './query.vue';
|
import debugQuery from './query.vue';
|
||||||
|
import response from './response.vue';
|
||||||
import debugRest from './rest.vue';
|
import debugRest from './rest.vue';
|
||||||
import debugSetting from './setting.vue';
|
import debugSetting from './setting.vue';
|
||||||
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
|
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
|
||||||
|
|
||||||
import { RequestBodyFormat, RequestComposition, RequestMethods } from '@/enums/apiEnum';
|
import type { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||||
|
import { RequestBodyFormat, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
module: string; // 当前激活的接口模块
|
||||||
|
moduleTree: ModuleTreeNode[]; // 接口模块树
|
||||||
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const initDefaultId = `debug-${Date.now()}`;
|
const initDefaultId = `debug-${Date.now()}`;
|
||||||
const activeTab = ref<string | number>(initDefaultId);
|
const activeRequestTab = ref<string | number>(initDefaultId);
|
||||||
const defaultBodyParams: BodyParams = {
|
const defaultBodyParams: BodyParams = {
|
||||||
format: RequestBodyFormat.NONE,
|
format: RequestBodyFormat.NONE,
|
||||||
formData: [],
|
formData: [],
|
||||||
|
@ -201,12 +226,14 @@
|
||||||
};
|
};
|
||||||
const defaultDebugParams = {
|
const defaultDebugParams = {
|
||||||
id: initDefaultId,
|
id: initDefaultId,
|
||||||
|
module: 'root',
|
||||||
moduleProtocol: 'http',
|
moduleProtocol: 'http',
|
||||||
|
url: '',
|
||||||
activeTab: RequestComposition.HEADER,
|
activeTab: RequestComposition.HEADER,
|
||||||
label: t('apiTestDebug.newApi'),
|
label: t('apiTestDebug.newApi'),
|
||||||
closable: true,
|
closable: true,
|
||||||
method: RequestMethods.GET,
|
method: RequestMethods.GET,
|
||||||
unSave: false,
|
unSaved: false,
|
||||||
headerParams: [],
|
headerParams: [],
|
||||||
bodyParams: cloneDeep(defaultBodyParams),
|
bodyParams: cloneDeep(defaultBodyParams),
|
||||||
queryParams: [],
|
queryParams: [],
|
||||||
|
@ -224,81 +251,74 @@
|
||||||
certificateAlias: '',
|
certificateAlias: '',
|
||||||
redirect: 'follow',
|
redirect: 'follow',
|
||||||
},
|
},
|
||||||
|
responseActiveTab: ResponseComposition.BODY,
|
||||||
response: {
|
response: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: [],
|
headers: [],
|
||||||
body: `{
|
timing: 12938,
|
||||||
"type": "team",
|
size: 8734,
|
||||||
"test": {
|
env: 'Mock',
|
||||||
"testPage": "tools/testing/run-tests.htm",
|
resource: '66',
|
||||||
"enabled": true
|
timingInfo: {
|
||||||
},
|
ready: 10,
|
||||||
"search": {
|
socketInit: 50,
|
||||||
"excludeFolders": [
|
dnsQuery: 20,
|
||||||
".git",
|
tcpHandshake: 80,
|
||||||
"node_modules",
|
sslHandshake: 40,
|
||||||
"tools/bin",
|
waitingTTFB: 30,
|
||||||
"tools/counts",
|
downloadContent: 10,
|
||||||
"tools/policheck",
|
deal: 10,
|
||||||
"tools/tfs_build_extensions",
|
total: 250,
|
||||||
"tools/testing/jscoverage",
|
},
|
||||||
"tools/testing/qunit",
|
extract: {
|
||||||
"tools/testing/chutzpah",
|
a: 'asdasd',
|
||||||
"server.net"
|
b: 'asdasdasd43f43',
|
||||||
]
|
},
|
||||||
},
|
console: `GET https://qa-release.fit2cloud.com/test`,
|
||||||
"languages": {
|
content: `请求地址:
|
||||||
"vs.languages.typescript": {
|
https://qa-release.fit2cloud.com/test
|
||||||
"validationSettings": [{
|
请求头:
|
||||||
"scope":"/",
|
Connection: keep-alive
|
||||||
"noImplicitAny":true,
|
Content-Length: 0
|
||||||
"noLib":false,
|
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||||
"extraLibs":[],
|
Host: qa-release.fit2cloud.com
|
||||||
"semanticValidation":true,
|
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
|
||||||
"syntaxValidation":true,
|
|
||||||
"codeGenTarget":"ES5",
|
Body:
|
||||||
"moduleGenTarget":"",
|
POST https://qa-release.fit2cloud.com/test
|
||||||
"lint": {
|
|
||||||
"emptyBlocksWithoutComment": "warning",
|
POST data:
|
||||||
"curlyBracketsMustNotBeOmitted": "warning",
|
|
||||||
"comparisonOperatorsNotStrict": "warning",
|
|
||||||
"missingSemicolon": "warning",
|
[no cookies]
|
||||||
"unknownTypeOfResults": "warning",
|
`,
|
||||||
"semicolonsInsteadOfBlocks": "warning",
|
header: `HTTP/ 1.1 200 OK
|
||||||
"functionsInsideLoops": "warning",
|
Content-Length: 2381
|
||||||
"functionsWithoutReturnType": "warning",
|
Content-Type: text/html
|
||||||
"tripleSlashReferenceAlike": "warning",
|
Server: bfe
|
||||||
"unusedImports": "warning",
|
Date: Wed, 13 Dec 2023 08:53:25 GMTHTTP/ 1.1 200 OK
|
||||||
"unusedVariables": "warning",
|
Content-Length: 2381
|
||||||
"unusedFunctions": "warning",
|
Content-Type: text/html
|
||||||
"unusedMembers": "warning"
|
Server: bfe
|
||||||
}
|
Date: Wed, 13 Dec 2023 08:53:25 GMT`,
|
||||||
},
|
body: `<?xml version="1.0"?>
|
||||||
{
|
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
|
||||||
"scope":"/client",
|
<connectionStrings>
|
||||||
"baseUrl":"/client",
|
<add name="MyDB"
|
||||||
"moduleGenTarget":"amd"
|
connectionString="value for the deployed Web.config file"
|
||||||
},
|
xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
|
||||||
{
|
</connectionStrings>
|
||||||
"scope":"/server",
|
<a>哈哈哈哈哈哈哈</a>
|
||||||
"moduleGenTarget":"commonjs"
|
<system.web>
|
||||||
},
|
<customErrors defaultRedirect="GenericError.htm"
|
||||||
{
|
mode="RemoteOnly" xdt:Transform="Replace">
|
||||||
"scope":"/build",
|
<error statusCode="500" redirect="InternalError.htm"/>
|
||||||
"moduleGenTarget":"commonjs"
|
</customErrors>
|
||||||
},
|
</system.web>
|
||||||
{
|
</configuration>`,
|
||||||
"scope":"/node_modules/nake",
|
|
||||||
"moduleGenTarget":"commonjs"
|
|
||||||
}],
|
|
||||||
"allowMultipleWorkers": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
}, // 调试返回的响应内容
|
}, // 调试返回的响应内容
|
||||||
};
|
};
|
||||||
const debugTabs = ref<TabItem[]>([cloneDeep(defaultDebugParams)]);
|
const debugTabs = ref<TabItem[]>([cloneDeep(defaultDebugParams)]);
|
||||||
const debugUrl = ref('');
|
|
||||||
const activeDebug = ref<TabItem>(debugTabs.value[0]);
|
const activeDebug = ref<TabItem>(debugTabs.value[0]);
|
||||||
|
|
||||||
function setActiveDebug(item: TabItem) {
|
function setActiveDebug(item: TabItem) {
|
||||||
|
@ -306,37 +326,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleActiveDebugChange() {
|
function handleActiveDebugChange() {
|
||||||
activeDebug.value.unSave = true;
|
activeDebug.value.unSaved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDebugTab() {
|
function addDebugTab() {
|
||||||
const id = `debug-${Date.now()}`;
|
const id = `debug-${Date.now()}`;
|
||||||
debugTabs.value.push({
|
debugTabs.value.push({
|
||||||
...cloneDeep(defaultDebugParams),
|
...cloneDeep(defaultDebugParams),
|
||||||
|
module: props.module,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
activeTab.value = id;
|
activeRequestTab.value = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDebugTab(tab: TabItem) {
|
function closeDebugTab(tab: TabItem) {
|
||||||
const index = debugTabs.value.findIndex((item) => item.id === tab.id);
|
const index = debugTabs.value.findIndex((item) => item.id === tab.id);
|
||||||
debugTabs.value.splice(index, 1);
|
debugTabs.value.splice(index, 1);
|
||||||
if (activeTab.value === tab.id) {
|
if (activeRequestTab.value === tab.id) {
|
||||||
activeTab.value = debugTabs.value[0]?.id || '';
|
activeRequestTab.value = debugTabs.value[0]?.id || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const moreActionList = [
|
const moreActionList = [
|
||||||
{
|
{
|
||||||
key: 'add',
|
eventTag: 'closeOther',
|
||||||
label: t('common.add'),
|
label: t('apiTestDebug.closeOther'),
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
label: t('common.delete'),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function handleMoreActionSelect(event: ActionsItem) {
|
||||||
|
if (event.eventTag === 'closeOther') {
|
||||||
|
debugTabs.value = debugTabs.value.filter((item) => item.id === activeRequestTab.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const contentTabList = [
|
const contentTabList = [
|
||||||
{
|
{
|
||||||
value: RequestComposition.HEADER,
|
value: RequestComposition.HEADER,
|
||||||
|
@ -427,16 +450,60 @@
|
||||||
splitBoxRef.value?.expand(0.6);
|
splitBoxRef.value?.expand(0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDebug() {
|
const saveModalVisible = ref(false);
|
||||||
activeDebug.value.unSave = false;
|
const saveModalForm = ref({
|
||||||
|
name: '',
|
||||||
|
url: activeDebug.value.url,
|
||||||
|
module: activeDebug.value.module,
|
||||||
|
});
|
||||||
|
const saveModalFormRef = ref<FormInstance>();
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => saveModalVisible.value,
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
saveModalFormRef.value?.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSaveShortcut() {
|
||||||
|
saveModalForm.value = {
|
||||||
|
name: '',
|
||||||
|
url: activeDebug.value.url,
|
||||||
|
module: activeDebug.value.module,
|
||||||
|
};
|
||||||
|
saveModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave(done: (closed: boolean) => void) {
|
||||||
|
saveModalFormRef.value?.validate(async (errors) => {
|
||||||
|
if (!errors) {
|
||||||
|
try {
|
||||||
|
saveLoading.value = true;
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
saveLoading.value = false;
|
||||||
|
saveModalVisible.value = false;
|
||||||
|
done(true);
|
||||||
|
activeDebug.value.unSaved = false;
|
||||||
|
Message.success(t('common.saveSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
done(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
registerCatchSaveShortcut(saveDebug);
|
registerCatchSaveShortcut(handleSaveShortcut);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
removeCatchSaveShortcut(saveDebug);
|
removeCatchSaveShortcut(handleSaveShortcut);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<condition
|
<condition
|
||||||
v-model:list="postConditions"
|
v-model:list="postConditions"
|
||||||
:condition-types="['script', 'sql', 'waitTime', 'extract']"
|
:condition-types="['script', 'sql', 'extract']"
|
||||||
add-text="apiTestDebug.postCondition"
|
add-text="apiTestDebug.postCondition"
|
||||||
:response="props.response"
|
:response="props.response"
|
||||||
:height-used="heightUsed"
|
:height-used="heightUsed"
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import condition from '../../../components/condition/index.vue';
|
import condition from '@/views/api-test/components/condition/index.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import condition from '../../../components/condition/index.vue';
|
import condition from '@/views/api-test/components/condition/index.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
|
|
||||||
import batchAddKeyVal from './batchAddKeyVal.vue';
|
import batchAddKeyVal from './batchAddKeyVal.vue';
|
||||||
|
import paramTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,301 @@
|
||||||
<template>
|
<template>
|
||||||
<div> </div>
|
<div class="flex h-full min-w-[300px] flex-col">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-[8px] bg-[var(--color-text-n9)] p-[8px_16px]">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<template v-if="props.activeLayout === 'vertical'">
|
||||||
|
<MsButton
|
||||||
|
v-if="props.isExpanded"
|
||||||
|
type="icon"
|
||||||
|
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
||||||
|
@click="emit('changeExpand', false)"
|
||||||
|
>
|
||||||
|
<icon-down :size="12" />
|
||||||
|
</MsButton>
|
||||||
|
<MsButton
|
||||||
|
v-else
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="!mr-0 !rounded-full"
|
||||||
|
@click="emit('changeExpand', true)"
|
||||||
|
>
|
||||||
|
<icon-right :size="12" />
|
||||||
|
</MsButton>
|
||||||
|
</template>
|
||||||
|
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
||||||
|
<a-radio-group
|
||||||
|
v-model:model-value="innerLayout"
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
@change="(val) => emit('changeLayout', val as Direction)"
|
||||||
|
>
|
||||||
|
<a-radio value="vertical">{{ t('apiTestDebug.vertical') }}</a-radio>
|
||||||
|
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.response.status" class="flex items-center justify-between gap-[24px]">
|
||||||
|
<a-popover position="left" content-class="response-popover-content">
|
||||||
|
<div class="text-[rgb(var(--danger-7))]">{{ props.response.status }}</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center gap-[8px] text-[14px]">
|
||||||
|
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
|
||||||
|
<div class="text-[rgb(var(--danger-7))]">{{ props.response.status }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover position="left" content-class="w-[400px]">
|
||||||
|
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.timing }} ms</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="mb-[8px] flex items-center gap-[8px] text-[14px]">
|
||||||
|
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div>
|
||||||
|
<div class="text-[rgb(var(--success-7))]">{{ props.response.timing }} ms</div>
|
||||||
|
</div>
|
||||||
|
<responseTimeLine :response-timing="$props.response.timingInfo" />
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover position="left" content-class="response-popover-content">
|
||||||
|
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.size }} bytes</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center gap-[8px] text-[14px]">
|
||||||
|
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
|
||||||
|
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.size }} bytes</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover position="left" content-class="response-popover-content">
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center gap-[8px] text-[14px]">
|
||||||
|
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.runningEnv') }}</div>
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover position="left" content-class="response-popover-content">
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center gap-[8px] text-[14px]">
|
||||||
|
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.resourcePool') }}</div>
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-[calc(100%-42px)] px-[16px] pb-[16px]">
|
||||||
|
<a-tabs v-model:active-key="activeTab" class="no-content">
|
||||||
|
<a-tab-pane v-for="item of responseTabList" :key="item.value" :title="item.label" />
|
||||||
|
</a-tabs>
|
||||||
|
<div class="response-container">
|
||||||
|
<MsCodeEditor
|
||||||
|
v-if="activeTab === ResponseComposition.BODY"
|
||||||
|
:model-value="props.response.body"
|
||||||
|
language="json"
|
||||||
|
theme="vs"
|
||||||
|
height="100%"
|
||||||
|
:languages="['json', 'html', 'xml', 'plaintext']"
|
||||||
|
:show-full-screen="false"
|
||||||
|
:show-theme-change="false"
|
||||||
|
show-language-change
|
||||||
|
show-charset-change
|
||||||
|
read-only
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<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>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
activeTab === 'HEADER' || activeTab === 'REAL_REQUEST' || activeTab === 'CONSOLE' || activeTab === '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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { useClipboard, useVModel } from '@vueuse/core';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import type { Direction } from '@/components/pure/ms-split-box/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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||||
|
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { ResponseTiming } from '@/models/apiTest/debug';
|
||||||
|
import { ResponseComposition } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
status: number;
|
||||||
|
timing: number;
|
||||||
|
size: number;
|
||||||
|
env: string;
|
||||||
|
resource: string;
|
||||||
|
body: string;
|
||||||
|
header: string;
|
||||||
|
content: string;
|
||||||
|
console: string;
|
||||||
|
extract: Record<string, any>;
|
||||||
|
timingInfo: ResponseTiming;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeTab: keyof typeof ResponseComposition;
|
||||||
|
activeLayout: Direction;
|
||||||
|
isExpanded: boolean;
|
||||||
|
response: Response;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:activeLayout', value: Direction): void;
|
||||||
|
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
|
||||||
|
(e: 'changeExpand', value: boolean): void;
|
||||||
|
(e: 'changeLayout', value: Direction): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const innerLayout = useVModel(props, 'activeLayout', emit);
|
||||||
|
const activeTab = useVModel(props, 'activeTab', emit);
|
||||||
|
|
||||||
|
const responseTabList = [
|
||||||
|
{
|
||||||
|
label: t('apiTestDebug.responseBody'),
|
||||||
|
value: ResponseComposition.BODY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('apiTestDebug.responseHeader'),
|
||||||
|
value: ResponseComposition.HEADER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('apiTestDebug.realRequest'),
|
||||||
|
value: ResponseComposition.REAL_REQUEST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('apiTestDebug.console'),
|
||||||
|
value: ResponseComposition.CONSOLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('apiTestDebug.extract'),
|
||||||
|
value: ResponseComposition.EXTRACT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('apiTestDebug.assertion'),
|
||||||
|
value: ResponseComposition.ASSERTION,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { copy, isSupported } = useClipboard();
|
||||||
|
|
||||||
|
function copyScript() {
|
||||||
|
if (isSupported) {
|
||||||
|
copy(props.response.body);
|
||||||
|
Message.success(t('common.copySuccess'));
|
||||||
|
} else {
|
||||||
|
Message.warning(t('apiTestDebug.copyNotSupport'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponsePreContent(type: keyof typeof ResponseComposition) {
|
||||||
|
switch (type) {
|
||||||
|
case ResponseComposition.HEADER:
|
||||||
|
return props.response.header.trim();
|
||||||
|
case ResponseComposition.REAL_REQUEST:
|
||||||
|
return props.response.content.trim();
|
||||||
|
case ResponseComposition.CONSOLE:
|
||||||
|
return props.response.console.trim();
|
||||||
|
case ResponseComposition.EXTRACT:
|
||||||
|
return Object.keys(props.response.extract)
|
||||||
|
.map((e) => `${e}: ${props.response.extract[e]}`)
|
||||||
|
.join('\n');
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: MsTableColumn = [
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.content',
|
||||||
|
dataIndex: 'content',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
slotName: 'status',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'desc',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
|
||||||
|
scroll: { x: '100%' },
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
propsRes.value.data = [
|
||||||
|
{
|
||||||
|
id: new Date().getTime(),
|
||||||
|
content: 'Response Code equals: 200',
|
||||||
|
status: 1,
|
||||||
|
desc: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: new Date().getTime(),
|
||||||
|
content: '$.users[1].age REGEX: 31',
|
||||||
|
status: 0,
|
||||||
|
desc: `Value expected to match regexp '31', but it did not match: '30' match: '30'`,
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.response-popover-content {
|
||||||
|
padding: 4px 8px;
|
||||||
|
.arco-popover-content {
|
||||||
|
@apply mt-0;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.response-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
height: calc(100% - 66px);
|
||||||
|
.response-header-pre {
|
||||||
|
@apply h-full overflow-auto bg-white;
|
||||||
|
.ms-scroll-bar();
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.arco-table-th) {
|
||||||
|
background-color: var(--color-text-n9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
|
|
||||||
import batchAddKeyVal from './batchAddKeyVal.vue';
|
import batchAddKeyVal from './batchAddKeyVal.vue';
|
||||||
|
import paramTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!props.isModal" class="folder">
|
<div class="folder">
|
||||||
<div class="folder-text">
|
<div class="folder-text">
|
||||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||||
<div class="folder-name">{{ t('apiTestDebug.allRequest') }}</div>
|
<div class="folder-name">{{ t('apiTestDebug.allRequest') }}</div>
|
||||||
|
@ -33,17 +33,18 @@
|
||||||
</popConfirm>
|
</popConfirm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-divider v-if="!props.isModal" class="my-[8px]" />
|
<a-divider class="my-[8px]" />
|
||||||
<a-spin class="min-h-[400px] w-full" :loading="loading">
|
<a-spin class="min-h-[400px] w-full" :loading="loading">
|
||||||
<MsTree
|
<MsTree
|
||||||
v-model:focus-node-key="focusNodeKey"
|
v-model:focus-node-key="focusNodeKey"
|
||||||
|
v-model:selected-keys="selectedKeys"
|
||||||
:data="folderTree"
|
:data="folderTree"
|
||||||
:keyword="moduleKeyword"
|
:keyword="moduleKeyword"
|
||||||
:node-more-actions="folderMoreActions"
|
:node-more-actions="folderMoreActions"
|
||||||
:default-expand-all="isExpandAll"
|
:default-expand-all="isExpandAll"
|
||||||
:expand-all="isExpandAll"
|
:expand-all="isExpandAll"
|
||||||
:empty-text="t('apiTestDebug.noMatchModule')"
|
:empty-text="t('apiTestDebug.noMatchModule')"
|
||||||
:draggable="!props.isModal"
|
:draggable="true"
|
||||||
:virtual-list-props="virtualListProps"
|
:virtual-list-props="virtualListProps"
|
||||||
:field-names="{
|
:field-names="{
|
||||||
title: 'name',
|
title: 'name',
|
||||||
|
@ -61,10 +62,10 @@
|
||||||
<template #title="nodeData">
|
<template #title="nodeData">
|
||||||
<div class="inline-flex w-full">
|
<div class="inline-flex w-full">
|
||||||
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||||
<div v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!props.isModal" #extra="nodeData">
|
<template #extra="nodeData">
|
||||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||||
<popConfirm
|
<popConfirm
|
||||||
v-if="nodeData.id !== 'root'"
|
v-if="nodeData.id !== 'root'"
|
||||||
|
@ -116,11 +117,10 @@
|
||||||
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isModal?: boolean; // 是否是弹窗模式
|
|
||||||
modulesCount?: Record<string, number>; // 模块数量统计对象
|
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||||
isExpandAll?: boolean; // 是否展开所有节点
|
isExpandAll?: boolean; // 是否展开所有节点
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits(['init', 'folderNodeSelect', 'newApi']);
|
const emit = defineEmits(['init', 'change', 'newApi']);
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -140,11 +140,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const virtualListProps = computed(() => {
|
const virtualListProps = computed(() => {
|
||||||
if (props.isModal) {
|
|
||||||
return {
|
|
||||||
height: 'calc(60vh - 190px)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
height: 'calc(100vh - 325px)',
|
height: 'calc(100vh - 325px)',
|
||||||
};
|
};
|
||||||
|
@ -169,8 +164,16 @@
|
||||||
const moduleKeyword = ref('');
|
const moduleKeyword = ref('');
|
||||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
const focusNodeKey = ref<string | number>('');
|
const focusNodeKey = ref<string | number>('');
|
||||||
|
const selectedKeys = ref<string[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedKeys.value,
|
||||||
|
(arr) => {
|
||||||
|
emit('change', arr[0]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function setFocusNodeKey(node: MsTreeNodeData) {
|
function setFocusNodeKey(node: MsTreeNodeData) {
|
||||||
focusNodeKey.value = node.id || '';
|
focusNodeKey.value = node.id || '';
|
||||||
}
|
}
|
||||||
|
@ -200,8 +203,8 @@
|
||||||
return {
|
return {
|
||||||
...e,
|
...e,
|
||||||
hideMoreAction: e.id === 'root',
|
hideMoreAction: e.id === 'root',
|
||||||
draggable: e.id !== 'root' && !props.isModal,
|
draggable: e.id !== 'root',
|
||||||
disabled: e.id === activeFolder.value && props.isModal,
|
disabled: e.id === activeFolder.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
emit('init', folderTree.value);
|
emit('init', folderTree.value);
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
<MsSplitBox :size="0.25" :max="0.5">
|
<MsSplitBox :size="0.25" :max="0.5">
|
||||||
<template #first>
|
<template #first>
|
||||||
<div class="p-[24px]">
|
<div class="p-[24px]">
|
||||||
<moduleTree @new-api="newApi" />
|
<moduleTree @init="(val) => (folderTree = val)" @new-api="newApi" @change="(val) => (activeModule = val)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #second>
|
<template #second>
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<debug ref="debugRef" />
|
<debug ref="debugRef" :module="activeModule" :module-tree="folderTree" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MsSplitBox>
|
</MsSplitBox>
|
||||||
|
@ -21,7 +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 { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||||
|
|
||||||
const debugRef = ref<InstanceType<typeof debug>>();
|
const debugRef = ref<InstanceType<typeof debug>>();
|
||||||
|
const activeModule = ref<string>('root');
|
||||||
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
|
||||||
function newApi() {
|
function newApi() {
|
||||||
debugRef.value?.addDebugTab();
|
debugRef.value?.addDebugTab();
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
||||||
'apiTestDebug.quote': '引用公共脚本',
|
'apiTestDebug.quote': '引用公共脚本',
|
||||||
'apiTestDebug.commonScriptList': '公共脚本列表',
|
'apiTestDebug.commonScriptList': '公共脚本列表',
|
||||||
'apiTestDebug.scriptEx': '脚本案例',
|
'apiTestDebug.scriptEx': '脚本案例',
|
||||||
'apiTestDebug.copyNotSupport': '您的浏览器不支持自动复制,请您手动复制脚本案例',
|
'apiTestDebug.copyNotSupport': '您的浏览器不支持自动复制,请您手动复制',
|
||||||
'apiTestDebug.scriptExCopySuccess': '脚本案例已复制',
|
'apiTestDebug.scriptExCopySuccess': '脚本案例已复制',
|
||||||
'apiTestDebug.parameters': '传递参数',
|
'apiTestDebug.parameters': '传递参数',
|
||||||
'apiTestDebug.scriptContent': '脚本内容',
|
'apiTestDebug.scriptContent': '脚本内容',
|
||||||
|
@ -137,4 +137,34 @@ export default {
|
||||||
'apiTestDebug.allMatch': '全部匹配',
|
'apiTestDebug.allMatch': '全部匹配',
|
||||||
'apiTestDebug.allMatchTip': '正则返回的是一个匹配结果数组',
|
'apiTestDebug.allMatchTip': '正则返回的是一个匹配结果数组',
|
||||||
'apiTestDebug.contentType': '响应内容格式',
|
'apiTestDebug.contentType': '响应内容格式',
|
||||||
|
'apiTestDebug.responseTime': '响应时间',
|
||||||
|
'apiTestDebug.responseStage': '阶段',
|
||||||
|
'apiTestDebug.time': '时间',
|
||||||
|
'apiTestDebug.ready': '准备',
|
||||||
|
'apiTestDebug.socketInit': 'Socket 初始化',
|
||||||
|
'apiTestDebug.dnsQuery': 'DNS 查询',
|
||||||
|
'apiTestDebug.tcpHandshake': 'TCP 握手',
|
||||||
|
'apiTestDebug.sslHandshake': 'SSL 握手',
|
||||||
|
'apiTestDebug.waitingTTFB': '等待中 (TTFB)',
|
||||||
|
'apiTestDebug.downloadContent': '下载内容',
|
||||||
|
'apiTestDebug.deal': '处理',
|
||||||
|
'apiTestDebug.total': '总共',
|
||||||
|
'apiTestDebug.responseBody': '响应体',
|
||||||
|
'apiTestDebug.responseHeader': '响应头',
|
||||||
|
'apiTestDebug.realRequest': '实际请求',
|
||||||
|
'apiTestDebug.console': '控制台',
|
||||||
|
'apiTestDebug.extract': '提取',
|
||||||
|
'apiTestDebug.statusCode': '状态码',
|
||||||
|
'apiTestDebug.responseSize': '响应大小',
|
||||||
|
'apiTestDebug.runningEnv': '运行环境',
|
||||||
|
'apiTestDebug.resourcePool': '资源池',
|
||||||
|
'apiTestDebug.content': '内容',
|
||||||
|
'apiTestDebug.status': '状态',
|
||||||
|
'apiTestDebug.requestName': '请求名称',
|
||||||
|
'apiTestDebug.requestNameRequired': '请求名称不能为空',
|
||||||
|
'apiTestDebug.requestNamePlaceholder': '请输入请求名称',
|
||||||
|
'apiTestDebug.requestUrl': '请求 URL',
|
||||||
|
'apiTestDebug.requestUrlRequired': '请求 URL不能为空',
|
||||||
|
'apiTestDebug.requestModule': '请求所属模块',
|
||||||
|
'apiTestDebug.closeOther': '关闭其他请求',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue