mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-12-02 20:19:16 +08:00
feat(全局): 接口调试-响应内容&部分组件调整
This commit is contained in:
parent
7e5967a688
commit
31562ac774
@ -1,4 +1,5 @@
|
||||
/** 主题变量覆盖 **/
|
||||
@border-radius-mini: 2px;
|
||||
@border-radius-small: 4px;
|
||||
@border-radius-medium: 6px;
|
||||
@border-radius-large: 12px;
|
||||
|
@ -1,15 +1,23 @@
|
||||
<template>
|
||||
<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>
|
||||
<div v-if="showTitleLine" class="mb-[12px] flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-[4px]">
|
||||
<a-select
|
||||
v-if="showLanguageChange"
|
||||
v-model:model-value="currentLanguage"
|
||||
:options="languageOptions"
|
||||
class="mr-[4px] w-[100px]"
|
||||
class="w-[100px]"
|
||||
size="small"
|
||||
@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
|
||||
v-if="showThemeChange"
|
||||
v-model:model-value="currentTheme"
|
||||
@ -34,9 +42,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 这里的 32px 是顶部标题的 32px -->
|
||||
<div :class="`flex ${showTitleLine ? 'h-[calc(100%-32px)]' : 'h-full'} w-full flex-row`">
|
||||
<div ref="codeEditBox" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
|
||||
<!-- 这里的 40px 是顶部标题的 40px -->
|
||||
<div :class="`flex ${showTitleLine ? 'h-[calc(100%-40px)]' : 'h-full'} w-full flex-row`">
|
||||
<div ref="codeContainerRef" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
|
||||
<slot name="rightBox"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,7 +54,9 @@
|
||||
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
|
||||
import { codeCharset } from '@/config/apiTest';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { decodeStringToCharset } from '@/utils';
|
||||
|
||||
import './userWorker';
|
||||
import MsCodeEditorTheme from './themes';
|
||||
@ -59,9 +69,34 @@
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
// 编辑器实例,每次调用组件都会创建独立的实例
|
||||
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
|
||||
const fullRef = ref<HTMLElement | null>();
|
||||
// 当前主题
|
||||
@ -77,6 +112,12 @@
|
||||
value: item,
|
||||
}))
|
||||
);
|
||||
function handleThemeChange(val: Theme) {
|
||||
editor.updateOptions({
|
||||
theme: val,
|
||||
});
|
||||
}
|
||||
|
||||
// 当前语言
|
||||
const currentLanguage = ref<Language>(props.language);
|
||||
// 语言选项
|
||||
@ -98,10 +139,29 @@
|
||||
};
|
||||
})
|
||||
.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(
|
||||
() => props.title || props.showThemeChange || props.showLanguageChange || props.showFullScreen
|
||||
() =>
|
||||
props.title ||
|
||||
props.showThemeChange ||
|
||||
props.showLanguageChange ||
|
||||
props.showCharsetChange ||
|
||||
props.showFullScreen
|
||||
);
|
||||
|
||||
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 codeBgEl = document.querySelector('.monaco-editor-background');
|
||||
if (codeBgEl) {
|
||||
@ -146,7 +179,7 @@
|
||||
|
||||
// 获取背景颜色
|
||||
const { backgroundColor } = computedStyle;
|
||||
codeEditBox.value.style.backgroundColor = backgroundColor;
|
||||
codeContainerRef.value.style.backgroundColor = backgroundColor;
|
||||
}
|
||||
};
|
||||
|
||||
@ -222,18 +255,21 @@
|
||||
});
|
||||
|
||||
return {
|
||||
codeEditBox,
|
||||
codeContainerRef,
|
||||
fullRef,
|
||||
isFullscreen,
|
||||
currentTheme,
|
||||
themeOptions,
|
||||
currentLanguage,
|
||||
languageOptions,
|
||||
currentCharset,
|
||||
charsetOptions,
|
||||
showTitleLine,
|
||||
toggle,
|
||||
t,
|
||||
handleThemeChange,
|
||||
handleLanguageChange,
|
||||
handleCharsetChange,
|
||||
insertContent,
|
||||
undo,
|
||||
redo,
|
||||
@ -244,11 +280,11 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ms-code-editor {
|
||||
@apply z-10;
|
||||
@apply z-10 overflow-hidden;
|
||||
|
||||
padding: 16px 0;
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
border-radius: var(--border-radius-small);
|
||||
&[data-mode-id='plaintext'] {
|
||||
:deep(.mtk1) {
|
||||
color: rgb(var(--primary-5));
|
||||
|
@ -94,10 +94,17 @@ export const editorProps = {
|
||||
type: Array as PropType<Array<Language>>,
|
||||
default: undefined,
|
||||
},
|
||||
// 是否代码语言切换
|
||||
showLanguageChange: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
// 是否显示字符集切换
|
||||
showCharsetChange: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
// 是否显示主题切换
|
||||
showThemeChange: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="tab-container">
|
||||
<div class="ms-editable-tab-container">
|
||||
<a-tooltip v-if="!isNotOverflow" :content="t('ms.editableTab.arrivedLeft')" :disabled="!arrivedState.left">
|
||||
<MsButton
|
||||
type="icon"
|
||||
@ -11,21 +11,22 @@
|
||||
<MsIcon type="icon-icon_left_outlined" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<div ref="tabNav" class="tab-nav">
|
||||
<div ref="tabNav" class="ms-editable-tab-nav">
|
||||
<div
|
||||
v-for="tab in props.tabs"
|
||||
:key="tab.id"
|
||||
class="tab"
|
||||
class="ms-editable-tab"
|
||||
:class="{ active: innerActiveTab === tab.id }"
|
||||
@click="handleTabClick(tab)"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<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
|
||||
v-if="tab.closable"
|
||||
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="tab-close-button"
|
||||
class="ms-editable-tab-close-button"
|
||||
@click="() => close(tab)"
|
||||
>
|
||||
<MsIcon type="icon-icon_close_outlined" size="12" />
|
||||
@ -37,7 +38,7 @@
|
||||
<MsButton
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="tab-button !mr-[8px]"
|
||||
class="ms-editable-tab-button !mr-[8px]"
|
||||
:disabled="arrivedState.right"
|
||||
@click="scrollTabs('right')"
|
||||
>
|
||||
@ -51,15 +52,19 @@
|
||||
<MsButton
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="tab-button !mr-[4px]"
|
||||
class="ms-editable-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">
|
||||
<MsButton type="icon" status="secondary" class="tab-button">
|
||||
<MsMoreAction
|
||||
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" />
|
||||
</MsButton>
|
||||
</MsMoreAction>
|
||||
@ -69,6 +74,7 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useScroll, useVModel } from '@vueuse/core';
|
||||
import { useDraggable } from 'vue-draggable-plus';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
@ -84,17 +90,21 @@
|
||||
activeTab: string | number;
|
||||
moreActionList?: ActionsItem[];
|
||||
limit?: number; // 最多可打开的tab数量
|
||||
atLeastOne?: boolean; // 是否至少保留一个tab
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:tabs', activeTab: string | number): void;
|
||||
(e: 'update:activeTab', activeTab: string | number): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'close', item: TabItem): void;
|
||||
(e: 'change', item: TabItem): void;
|
||||
(e: 'moreActionSelect', item: ActionsItem): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerActiveTab = useVModel(props, 'activeTab', emit);
|
||||
const innerTabs = useVModel(props, 'tabs', emit);
|
||||
const tabNav = ref<HTMLElement | null>(null);
|
||||
const { arrivedState } = useScroll(tabNav);
|
||||
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
||||
@ -139,6 +149,9 @@
|
||||
);
|
||||
|
||||
watch(props.tabs, () => {
|
||||
useDraggable('.ms-editable-tab-nav', innerTabs, {
|
||||
ghostClass: 'ms-editable-tab-ghost',
|
||||
});
|
||||
nextTick(() => {
|
||||
scrollToActiveTab();
|
||||
});
|
||||
@ -169,17 +182,17 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tab-container {
|
||||
.ms-editable-tab-container {
|
||||
@apply flex items-center;
|
||||
|
||||
height: 32px;
|
||||
.tab-nav {
|
||||
.ms-editable-tab-nav {
|
||||
@apply relative flex overflow-x-auto whitespace-nowrap;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0; /* 宽度为0,隐藏垂直滚动条 */
|
||||
height: 0; /* 高度为0,隐藏水平滚动条 */
|
||||
}
|
||||
.tab {
|
||||
.ms-editable-tab {
|
||||
@apply flex cursor-pointer items-center;
|
||||
|
||||
margin-right: 4px;
|
||||
@ -191,18 +204,18 @@
|
||||
&:hover {
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
.tab-close-button {
|
||||
.ms-editable-tab-close-button {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
.tab-close-button {
|
||||
.ms-editable-tab-close-button {
|
||||
@apply invisible !rounded-full;
|
||||
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab-button {
|
||||
.ms-editable-tab-button {
|
||||
padding: 8px;
|
||||
&:not([disabled='true']) {
|
||||
padding: 8px;
|
||||
@ -213,4 +226,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.ms-editable-tab-ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
@ -2,5 +2,6 @@ export interface TabItem {
|
||||
id: string | number;
|
||||
label: string;
|
||||
closable?: boolean;
|
||||
unSaved?: boolean; // 未保存
|
||||
[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>
|
||||
<div v-if="parsedXml" class="container">
|
||||
<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 v-if="parsedXml" class="container">
|
||||
<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 v-if="!isValidXml">{{ t('ms.jsonpathPicker.xmlNotValid') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { XpathNode } from './types';
|
||||
import * as XmlBeautify from 'xml-beautify';
|
||||
|
||||
const props = defineProps<{
|
||||
xmlString: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['pick']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const parsedXml = ref<Document | null>(null);
|
||||
const flattenedXml = ref<XpathNode[]>([]);
|
||||
const tempXmls = ref<XpathNode[]>([]);
|
||||
const isValidXml = ref(true); // 是否是合法的xml
|
||||
|
||||
/**
|
||||
* 获取同名兄弟节点
|
||||
@ -68,6 +75,7 @@
|
||||
emit('pick', xpath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析xml
|
||||
*/
|
||||
@ -75,6 +83,12 @@
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
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;
|
||||
// 先将 XML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
||||
flattenedXml.value = new XmlBeautify({ parser: DOMParser })
|
||||
@ -88,7 +102,7 @@
|
||||
flattenXml(xmlDoc.documentElement, '');
|
||||
// 将扁平化后的 XML 字符串中的每个节点的 xpath 替换为真实的 xpath
|
||||
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) {
|
||||
const { xpath } = tempXmls.value[targetNodeIndex];
|
||||
tempXmls.value.splice(targetNodeIndex, 1); // 匹配成功后,将匹配到的节点从 tempXmls 中删除,避免重复匹配
|
||||
|
@ -90,6 +90,14 @@ export default {
|
||||
priority: 'Priority',
|
||||
tag: 'Tag',
|
||||
},
|
||||
tag: {
|
||||
case: 'Case',
|
||||
module: 'Module',
|
||||
precondition: 'Precondition',
|
||||
desc: 'Step desc',
|
||||
expect: 'Expected result',
|
||||
remark: 'Remark',
|
||||
},
|
||||
hotboxMenu: {
|
||||
expand: 'Expand/Collapse',
|
||||
insetParent: 'Insert one level up',
|
||||
|
@ -84,6 +84,14 @@ export default {
|
||||
priority: '优先级',
|
||||
tag: '标签',
|
||||
},
|
||||
tag: {
|
||||
case: '用例',
|
||||
module: '模块',
|
||||
precondition: '前置条件',
|
||||
desc: '步骤描述',
|
||||
expect: '预期结果',
|
||||
remark: '备注',
|
||||
},
|
||||
hotboxMenu: {
|
||||
expand: '展开/收起',
|
||||
insetParent: '插入上一级',
|
||||
|
@ -67,12 +67,14 @@
|
||||
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
|
||||
export type Direction = 'horizontal' | 'vertical';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size?: number | string; // 左侧宽度/顶部容器高度。expandDirection为 right 时,size 也是左侧容器宽度,所以想要缩小右侧容器宽度只需要将 size 调大即可
|
||||
min?: number | string;
|
||||
max?: number | string;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
direction?: Direction;
|
||||
expandDirection?: 'left' | 'right' | 'top'; // TODO: 未实现 bottom,有场景再补充。目前默认水平是 left,垂直是 top
|
||||
disabled?: boolean; // 是否禁用
|
||||
firstContainerClass?: string; // first容器类名
|
||||
@ -214,7 +216,9 @@
|
||||
@apply h-full bg-white;
|
||||
}
|
||||
.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 {
|
||||
@apply absolute w-full bg-transparent;
|
||||
|
||||
|
@ -8,5 +8,5 @@ export const conditionTypeNameMap = {
|
||||
waitTime: 'apiTestDebug.waitTime',
|
||||
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,
|
||||
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',
|
||||
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.expandAll': 'Expand all',
|
||||
'common.copy': 'Copy',
|
||||
'common.copySuccess': 'Copy successfully',
|
||||
'common.fork': 'Fork',
|
||||
'common.forked': 'Forked',
|
||||
'common.more': 'More',
|
||||
@ -95,4 +96,6 @@ export default {
|
||||
'common.revoke': 'Revoke',
|
||||
'common.clear': 'Clear',
|
||||
'common.tag': 'Tag',
|
||||
'common.success': 'Success',
|
||||
'common.fail': 'Failed',
|
||||
};
|
||||
|
@ -75,6 +75,7 @@ export default {
|
||||
'common.collapseAll': '收起全部',
|
||||
'common.expandAll': '展开全部',
|
||||
'common.copy': '复制',
|
||||
'common.copySuccess': '复制成功',
|
||||
'common.fork': '关注',
|
||||
'common.forked': '已关注',
|
||||
'common.more': '更多',
|
||||
@ -98,4 +99,6 @@ export default {
|
||||
'common.revoke': '撤销',
|
||||
'common.clear': '清空',
|
||||
'common.tag': '标签',
|
||||
'common.success': '成功',
|
||||
'common.fail': '失败',
|
||||
};
|
||||
|
@ -11,3 +11,15 @@ export interface ExpressionConfig {
|
||||
specifyMatchNum?: number; // 指定匹配下标
|
||||
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 { codeCharset } from '@/config/apiTest';
|
||||
|
||||
import { isObject } from './is';
|
||||
|
||||
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
||||
@ -324,7 +326,7 @@ export const getHashParameters = (): Record<string, string> => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 id 序列号
|
||||
* 生成 id 序列号
|
||||
* @returns
|
||||
*/
|
||||
export const getGenerateId = () => {
|
||||
@ -357,3 +359,14 @@ export const downloadByteFile = (byte: BlobPart, fileName: string) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
.condition-content {
|
||||
@apply flex-1 overflow-y-auto;
|
||||
.ms-scroll-bar();
|
||||
|
@ -257,7 +257,7 @@
|
||||
}
|
||||
.code-container {
|
||||
padding: 12px;
|
||||
height: 400px;
|
||||
max-height: 400px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
|
@ -567,7 +567,7 @@
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
:deep(.arco-table-cell-align-left) {
|
||||
padding: 16px 12px;
|
||||
padding: 16px 2px;
|
||||
}
|
||||
:deep(.arco-table-td) {
|
||||
.arco-table-cell {
|
||||
|
97
frontend/src/views/api-test/components/responseTimeLine.vue
Normal file
97
frontend/src/views/api-test/components/responseTimeLine.vue
Normal file
@ -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"
|
||||
class="flex-1"
|
||||
theme="MS-text"
|
||||
height="calc(100% - 12px)"
|
||||
height="100%"
|
||||
:show-full-screen="false"
|
||||
>
|
||||
<template #title>
|
||||
|
@ -51,7 +51,7 @@
|
||||
v-model:model-value="currentBodyCode"
|
||||
class="flex-1"
|
||||
theme="vs-dark"
|
||||
height="calc(100% - 12px)"
|
||||
height="100%"
|
||||
:show-full-screen="false"
|
||||
:language="currentCodeLanguage"
|
||||
>
|
||||
|
@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeTab"
|
||||
:tabs="debugTabs"
|
||||
v-model:active-tab="activeRequestTab"
|
||||
v-model:tabs="debugTabs"
|
||||
:more-action-list="moreActionList"
|
||||
at-least-one
|
||||
@add="addDebugTab"
|
||||
@close="closeDebugTab"
|
||||
@change="setActiveDebug"
|
||||
@more-action-select="handleMoreActionSelect"
|
||||
>
|
||||
<template #label="{ tab }">
|
||||
<apiMethodName :method="tab.method" class="mr-[4px]" />
|
||||
{{ tab.label }}
|
||||
<div v-if="tab.unSave" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
</div>
|
||||
@ -34,7 +35,7 @@
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-input
|
||||
v-model:model-value="debugUrl"
|
||||
v-model:model-value="activeDebug.url"
|
||||
:placeholder="t('apiTestDebug.urlPlaceholder')"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
@ -129,65 +130,89 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #second>
|
||||
<div class="min-w-[290px] bg-[var(--color-text-n9)] p-[8px_16px]">
|
||||
<div class="flex items-center">
|
||||
<template v-if="activeLayout === 'vertical'">
|
||||
<MsButton
|
||||
v-if="isExpanded"
|
||||
type="icon"
|
||||
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
||||
@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>
|
||||
<response
|
||||
v-model:active-layout="activeLayout"
|
||||
v-model:active-tab="activeDebug.responseActiveTab"
|
||||
:is-expanded="isExpanded"
|
||||
:response="activeDebug.response"
|
||||
@change-expand="changeExpand"
|
||||
@change-layout="handleActiveLayoutChange"
|
||||
/>
|
||||
</template>
|
||||
</MsSplitBox>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
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 { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
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 debugBody, { BodyParams } from './body.vue';
|
||||
import debugHeader from './header.vue';
|
||||
import postcondition from './postcondition.vue';
|
||||
import precondition from './precondition.vue';
|
||||
import debugQuery from './query.vue';
|
||||
import response from './response.vue';
|
||||
import debugRest from './rest.vue';
|
||||
import debugSetting from './setting.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
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 initDefaultId = `debug-${Date.now()}`;
|
||||
const activeTab = ref<string | number>(initDefaultId);
|
||||
const activeRequestTab = ref<string | number>(initDefaultId);
|
||||
const defaultBodyParams: BodyParams = {
|
||||
format: RequestBodyFormat.NONE,
|
||||
formData: [],
|
||||
@ -201,12 +226,14 @@
|
||||
};
|
||||
const defaultDebugParams = {
|
||||
id: initDefaultId,
|
||||
module: 'root',
|
||||
moduleProtocol: 'http',
|
||||
url: '',
|
||||
activeTab: RequestComposition.HEADER,
|
||||
label: t('apiTestDebug.newApi'),
|
||||
closable: true,
|
||||
method: RequestMethods.GET,
|
||||
unSave: false,
|
||||
unSaved: false,
|
||||
headerParams: [],
|
||||
bodyParams: cloneDeep(defaultBodyParams),
|
||||
queryParams: [],
|
||||
@ -224,81 +251,74 @@
|
||||
certificateAlias: '',
|
||||
redirect: 'follow',
|
||||
},
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
response: {
|
||||
status: 200,
|
||||
headers: [],
|
||||
body: `{
|
||||
"type": "team",
|
||||
"test": {
|
||||
"testPage": "tools/testing/run-tests.htm",
|
||||
"enabled": true
|
||||
},
|
||||
"search": {
|
||||
"excludeFolders": [
|
||||
".git",
|
||||
"node_modules",
|
||||
"tools/bin",
|
||||
"tools/counts",
|
||||
"tools/policheck",
|
||||
"tools/tfs_build_extensions",
|
||||
"tools/testing/jscoverage",
|
||||
"tools/testing/qunit",
|
||||
"tools/testing/chutzpah",
|
||||
"server.net"
|
||||
]
|
||||
},
|
||||
"languages": {
|
||||
"vs.languages.typescript": {
|
||||
"validationSettings": [{
|
||||
"scope":"/",
|
||||
"noImplicitAny":true,
|
||||
"noLib":false,
|
||||
"extraLibs":[],
|
||||
"semanticValidation":true,
|
||||
"syntaxValidation":true,
|
||||
"codeGenTarget":"ES5",
|
||||
"moduleGenTarget":"",
|
||||
"lint": {
|
||||
"emptyBlocksWithoutComment": "warning",
|
||||
"curlyBracketsMustNotBeOmitted": "warning",
|
||||
"comparisonOperatorsNotStrict": "warning",
|
||||
"missingSemicolon": "warning",
|
||||
"unknownTypeOfResults": "warning",
|
||||
"semicolonsInsteadOfBlocks": "warning",
|
||||
"functionsInsideLoops": "warning",
|
||||
"functionsWithoutReturnType": "warning",
|
||||
"tripleSlashReferenceAlike": "warning",
|
||||
"unusedImports": "warning",
|
||||
"unusedVariables": "warning",
|
||||
"unusedFunctions": "warning",
|
||||
"unusedMembers": "warning"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope":"/client",
|
||||
"baseUrl":"/client",
|
||||
"moduleGenTarget":"amd"
|
||||
},
|
||||
{
|
||||
"scope":"/server",
|
||||
"moduleGenTarget":"commonjs"
|
||||
},
|
||||
{
|
||||
"scope":"/build",
|
||||
"moduleGenTarget":"commonjs"
|
||||
},
|
||||
{
|
||||
"scope":"/node_modules/nake",
|
||||
"moduleGenTarget":"commonjs"
|
||||
}],
|
||||
"allowMultipleWorkers": true
|
||||
}
|
||||
}
|
||||
}`,
|
||||
timing: 12938,
|
||||
size: 8734,
|
||||
env: 'Mock',
|
||||
resource: '66',
|
||||
timingInfo: {
|
||||
ready: 10,
|
||||
socketInit: 50,
|
||||
dnsQuery: 20,
|
||||
tcpHandshake: 80,
|
||||
sslHandshake: 40,
|
||||
waitingTTFB: 30,
|
||||
downloadContent: 10,
|
||||
deal: 10,
|
||||
total: 250,
|
||||
},
|
||||
extract: {
|
||||
a: 'asdasd',
|
||||
b: 'asdasdasd43f43',
|
||||
},
|
||||
console: `GET https://qa-release.fit2cloud.com/test`,
|
||||
content: `请求地址:
|
||||
https://qa-release.fit2cloud.com/test
|
||||
请求头:
|
||||
Connection: keep-alive
|
||||
Content-Length: 0
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Host: qa-release.fit2cloud.com
|
||||
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
|
||||
|
||||
Body:
|
||||
POST https://qa-release.fit2cloud.com/test
|
||||
|
||||
POST data:
|
||||
|
||||
|
||||
[no cookies]
|
||||
`,
|
||||
header: `HTTP/ 1.1 200 OK
|
||||
Content-Length: 2381
|
||||
Content-Type: text/html
|
||||
Server: bfe
|
||||
Date: Wed, 13 Dec 2023 08:53:25 GMTHTTP/ 1.1 200 OK
|
||||
Content-Length: 2381
|
||||
Content-Type: text/html
|
||||
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">
|
||||
<connectionStrings>
|
||||
<add name="MyDB"
|
||||
connectionString="value for the deployed Web.config file"
|
||||
xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
|
||||
</connectionStrings>
|
||||
<a>哈哈哈哈哈哈哈</a>
|
||||
<system.web>
|
||||
<customErrors defaultRedirect="GenericError.htm"
|
||||
mode="RemoteOnly" xdt:Transform="Replace">
|
||||
<error statusCode="500" redirect="InternalError.htm"/>
|
||||
</customErrors>
|
||||
</system.web>
|
||||
</configuration>`,
|
||||
}, // 调试返回的响应内容
|
||||
};
|
||||
const debugTabs = ref<TabItem[]>([cloneDeep(defaultDebugParams)]);
|
||||
const debugUrl = ref('');
|
||||
const activeDebug = ref<TabItem>(debugTabs.value[0]);
|
||||
|
||||
function setActiveDebug(item: TabItem) {
|
||||
@ -306,37 +326,40 @@
|
||||
}
|
||||
|
||||
function handleActiveDebugChange() {
|
||||
activeDebug.value.unSave = true;
|
||||
activeDebug.value.unSaved = true;
|
||||
}
|
||||
|
||||
function addDebugTab() {
|
||||
const id = `debug-${Date.now()}`;
|
||||
debugTabs.value.push({
|
||||
...cloneDeep(defaultDebugParams),
|
||||
module: props.module,
|
||||
id,
|
||||
});
|
||||
activeTab.value = id;
|
||||
activeRequestTab.value = id;
|
||||
}
|
||||
|
||||
function closeDebugTab(tab: TabItem) {
|
||||
const index = debugTabs.value.findIndex((item) => item.id === tab.id);
|
||||
debugTabs.value.splice(index, 1);
|
||||
if (activeTab.value === tab.id) {
|
||||
activeTab.value = debugTabs.value[0]?.id || '';
|
||||
if (activeRequestTab.value === tab.id) {
|
||||
activeRequestTab.value = debugTabs.value[0]?.id || '';
|
||||
}
|
||||
}
|
||||
|
||||
const moreActionList = [
|
||||
{
|
||||
key: 'add',
|
||||
label: t('common.add'),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('common.delete'),
|
||||
eventTag: 'closeOther',
|
||||
label: t('apiTestDebug.closeOther'),
|
||||
},
|
||||
];
|
||||
|
||||
function handleMoreActionSelect(event: ActionsItem) {
|
||||
if (event.eventTag === 'closeOther') {
|
||||
debugTabs.value = debugTabs.value.filter((item) => item.id === activeRequestTab.value);
|
||||
}
|
||||
}
|
||||
|
||||
const contentTabList = [
|
||||
{
|
||||
value: RequestComposition.HEADER,
|
||||
@ -427,16 +450,60 @@
|
||||
splitBoxRef.value?.expand(0.6);
|
||||
}
|
||||
|
||||
function saveDebug() {
|
||||
activeDebug.value.unSave = false;
|
||||
const saveModalVisible = ref(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(() => {
|
||||
registerCatchSaveShortcut(saveDebug);
|
||||
registerCatchSaveShortcut(handleSaveShortcut);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeCatchSaveShortcut(saveDebug);
|
||||
removeCatchSaveShortcut(handleSaveShortcut);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<condition
|
||||
v-model:list="postConditions"
|
||||
:condition-types="['script', 'sql', 'waitTime', 'extract']"
|
||||
:condition-types="['script', 'sql', 'extract']"
|
||||
add-text="apiTestDebug.postCondition"
|
||||
:response="props.response"
|
||||
:height-used="heightUsed"
|
||||
@ -23,7 +23,7 @@
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
|
||||
|
@ -23,8 +23,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
|
||||
import batchAddKeyVal from './batchAddKeyVal.vue';
|
||||
import paramTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
|
@ -1,7 +1,301 @@
|
||||
<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>
|
||||
|
||||
<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">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
|
||||
import batchAddKeyVal from './batchAddKeyVal.vue';
|
||||
import paramTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div v-if="!props.isModal" class="folder">
|
||||
<div class="folder">
|
||||
<div class="folder-text">
|
||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||
<div class="folder-name">{{ t('apiTestDebug.allRequest') }}</div>
|
||||
@ -33,17 +33,18 @@
|
||||
</popConfirm>
|
||||
</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">
|
||||
<MsTree
|
||||
v-model:focus-node-key="focusNodeKey"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:data="folderTree"
|
||||
:keyword="moduleKeyword"
|
||||
:node-more-actions="folderMoreActions"
|
||||
:default-expand-all="isExpandAll"
|
||||
:expand-all="isExpandAll"
|
||||
:empty-text="t('apiTestDebug.noMatchModule')"
|
||||
:draggable="!props.isModal"
|
||||
:draggable="true"
|
||||
:virtual-list-props="virtualListProps"
|
||||
:field-names="{
|
||||
title: 'name',
|
||||
@ -61,10 +62,10 @@
|
||||
<template #title="nodeData">
|
||||
<div class="inline-flex w-full">
|
||||
<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>
|
||||
</template>
|
||||
<template v-if="!props.isModal" #extra="nodeData">
|
||||
<template #extra="nodeData">
|
||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
@ -116,11 +117,10 @@
|
||||
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||
|
||||
const props = defineProps<{
|
||||
isModal?: boolean; // 是否是弹窗模式
|
||||
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||
isExpandAll?: boolean; // 是否展开所有节点
|
||||
}>();
|
||||
const emit = defineEmits(['init', 'folderNodeSelect', 'newApi']);
|
||||
const emit = defineEmits(['init', 'change', 'newApi']);
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
@ -140,11 +140,6 @@
|
||||
}
|
||||
|
||||
const virtualListProps = computed(() => {
|
||||
if (props.isModal) {
|
||||
return {
|
||||
height: 'calc(60vh - 190px)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
height: 'calc(100vh - 325px)',
|
||||
};
|
||||
@ -169,8 +164,16 @@
|
||||
const moduleKeyword = ref('');
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
const focusNodeKey = ref<string | number>('');
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
watch(
|
||||
() => selectedKeys.value,
|
||||
(arr) => {
|
||||
emit('change', arr[0]);
|
||||
}
|
||||
);
|
||||
|
||||
function setFocusNodeKey(node: MsTreeNodeData) {
|
||||
focusNodeKey.value = node.id || '';
|
||||
}
|
||||
@ -200,8 +203,8 @@
|
||||
return {
|
||||
...e,
|
||||
hideMoreAction: e.id === 'root',
|
||||
draggable: e.id !== 'root' && !props.isModal,
|
||||
disabled: e.id === activeFolder.value && props.isModal,
|
||||
draggable: e.id !== 'root',
|
||||
disabled: e.id === activeFolder.value,
|
||||
};
|
||||
});
|
||||
emit('init', folderTree.value);
|
||||
|
@ -3,12 +3,12 @@
|
||||
<MsSplitBox :size="0.25" :max="0.5">
|
||||
<template #first>
|
||||
<div class="p-[24px]">
|
||||
<moduleTree @new-api="newApi" />
|
||||
<moduleTree @init="(val) => (folderTree = val)" @new-api="newApi" @change="(val) => (activeModule = val)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #second>
|
||||
<div class="flex h-full flex-col">
|
||||
<debug ref="debugRef" />
|
||||
<debug ref="debugRef" :module="activeModule" :module-tree="folderTree" />
|
||||
</div>
|
||||
</template>
|
||||
</MsSplitBox>
|
||||
@ -21,7 +21,11 @@
|
||||
import debug from './components/debug/index.vue';
|
||||
import moduleTree from './components/moduleTree.vue';
|
||||
|
||||
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||
|
||||
const debugRef = ref<InstanceType<typeof debug>>();
|
||||
const activeModule = ref<string>('root');
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
|
||||
function newApi() {
|
||||
debugRef.value?.addDebugTab();
|
||||
|
@ -68,7 +68,7 @@ export default {
|
||||
'apiTestDebug.quote': '引用公共脚本',
|
||||
'apiTestDebug.commonScriptList': '公共脚本列表',
|
||||
'apiTestDebug.scriptEx': '脚本案例',
|
||||
'apiTestDebug.copyNotSupport': '您的浏览器不支持自动复制,请您手动复制脚本案例',
|
||||
'apiTestDebug.copyNotSupport': '您的浏览器不支持自动复制,请您手动复制',
|
||||
'apiTestDebug.scriptExCopySuccess': '脚本案例已复制',
|
||||
'apiTestDebug.parameters': '传递参数',
|
||||
'apiTestDebug.scriptContent': '脚本内容',
|
||||
@ -137,4 +137,34 @@ export default {
|
||||
'apiTestDebug.allMatch': '全部匹配',
|
||||
'apiTestDebug.allMatchTip': '正则返回的是一个匹配结果数组',
|
||||
'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
Block a user