feat(全局): 接口调试-响应内容&部分组件调整

This commit is contained in:
baiqi 2024-01-22 19:41:53 +08:00 committed by Craftsman
parent 7e5967a688
commit 31562ac774
33 changed files with 861 additions and 213 deletions

View File

@ -1,4 +1,5 @@
/** 主题变量覆盖 **/
@border-radius-mini: 2px;
@border-radius-small: 4px;
@border-radius-medium: 6px;
@border-radius-large: 12px;

View File

@ -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));

View File

@ -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,

View File

@ -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>

View File

@ -2,5 +2,6 @@ export interface TabItem {
id: string | number;
label: string;
closable?: boolean;
unSaved?: boolean; // 未保存
[key: string]: any;
}

View File

@ -0,0 +1,3 @@
export default {
'ms.jsonpathPicker.xmlNotValid': '非法的XML文本',
};

View File

@ -0,0 +1,3 @@
export default {
'ms.jsonpathPicker.xmlNotValid': '非法的XML文本',
};

View File

@ -1,24 +1,31 @@
<template>
<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(`&lt;${t.content}`));
const targetNodeIndex = tempXmls.value.findIndex((txt) => e.content.includes(`&lt;${txt.content}`));
if (targetNodeIndex >= 0) {
const { xpath } = tempXmls.value[targetNodeIndex];
tempXmls.value.splice(targetNodeIndex, 1); // tempXmls

View File

@ -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',

View File

@ -84,6 +84,14 @@ export default {
priority: '优先级',
tag: '标签',
},
tag: {
case: '用例',
module: '模块',
precondition: '前置条件',
desc: '步骤描述',
expect: '预期结果',
remark: '备注',
},
hotboxMenu: {
expand: '展开/收起',
insetParent: '插入上一级',

View File

@ -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;

View File

@ -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'];

View File

@ -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',
};

View File

@ -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',
}

View File

@ -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',
};

View File

@ -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': '失败',
};

View File

@ -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;
}

View File

@ -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));
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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 {

View 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>

View File

@ -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>

View File

@ -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"
>

View File

@ -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
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,
},
"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"
]
extract: {
a: 'asdasd',
b: 'asdasdasd43f43',
},
"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
}
}
}`,
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({

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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>

View File

@ -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';

View File

@ -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);

View File

@ -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();

View File

@ -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': '关闭其他请求',
};