feat(全局): 接口调试-后置条件&部分组件调整

This commit is contained in:
baiqi 2024-01-20 15:30:12 +08:00 committed by Craftsman
parent 1223602106
commit 6ee926c091
107 changed files with 2484 additions and 1009 deletions

View File

@ -76,6 +76,7 @@ module.exports = {
'no-underscore-dangle': 'off',
'vue/attributes-order': 1,
'simple-import-sort/exports': 'error',
'no-case-declarations': 'off',
// 调整导入语句的顺序
'simple-import-sort/imports': [
'error',
@ -97,6 +98,7 @@ module.exports = {
'^color$',
'^localforage$',
'vue-draggable-plus',
'jsonpath-plus',
], // node依赖
['.*/assets/.*', '^@/assets$'], // 项目静态资源
['^@/components/pure/.*', '^@/components/business/.*', '.*\\.vue$'], // 组件

View File

@ -48,6 +48,7 @@
"@tiptap/vue-3": "^2.1.13",
"@types/color": "^3.0.4",
"@vueuse/core": "^10.4.1",
"@xmldom/xmldom": "^0.8.10",
"ace-builds": "^1.24.2",
"ahooks-vue": "^0.15.1",
"axios": "^1.6.5",
@ -56,6 +57,7 @@
"fastq": "^1.15.0",
"hotbox-minder": "1.0.15",
"jsencrypt": "^3.3.2",
"jsonpath-plus": "^8.0.0",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
@ -75,7 +77,9 @@
"vue-i18n": "^9.3.0",
"vue-router": "^4.2.4",
"vue3-ace-editor": "^2.2.3",
"vue3-colorpicker": "^2.2.2"
"vue3-colorpicker": "^2.2.2",
"xml-beautify": "^1.2.3",
"xpath": "^0.0.34"
},
"devDependencies": {
"@arco-plugins/vite-vue": "^1.4.5",

View File

@ -1,17 +1,16 @@
import MSR from '@/api/http/index';
import { GetApiTestList, GetApiTestListUrl } from '@/api/requrls/api-test';
import { APIListItemI } from '@/models/api-test';
import { CommonList, TableQueryParams } from '@/models/common';
export function getTableList(params: TableQueryParams) {
const { current, pageSize, sort, filter, keyword } = params;
return MSR.post<CommonList<APIListItemI>>({
return MSR.post<CommonList<any>>({
url: GetApiTestList,
data: { current, pageSize, sort, filter, keyword, projectId: 'test-project-id' },
});
}
export function getlist() {
return MSR.get<CommonList<APIListItemI>>({ url: GetApiTestListUrl });
return MSR.get<CommonList<any>>({ url: GetApiTestListUrl });
}

View File

@ -1,7 +1,7 @@
@font-face {
font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1702433539155') format('woff2'), url('iconfont.woff?t=1702433539155') format('woff'),
url('iconfont.ttf?t=1702433539155') format('truetype'), url('iconfont.svg?t=1702433539155#iconfont') format('svg');
src: url('iconfont.woff2?t=1705549750803') format('woff2'), url('iconfont.woff?t=1705549750803') format('woff'),
url('iconfont.ttf?t=1705549750803') format('truetype'), url('iconfont.svg?t=1705549750803#iconfont') format('svg');
}
.iconfont {
font-size: 16px;
@ -10,6 +10,18 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-icon_carriage_return2::before {
content: '\e79a';
}
.icon-icon_carriage_return1::before {
content: '\e799';
}
.icon-icon_swagger::before {
content: '\e798';
}
.icon-a-icon_file-json::before {
content: '\e797';
}
.icon-icon_keyboard::before {
content: '\e796';
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,34 @@
"css_prefix_text": "icon-",
"description": "DE、MS项目icon管理",
"glyphs": [
{
"icon_id": "38923289",
"name": "icon_carriage_return",
"font_class": "icon_carriage_return2",
"unicode": "e79a",
"unicode_decimal": 59290
},
{
"icon_id": "38923258",
"name": "icon_delete",
"font_class": "icon_carriage_return1",
"unicode": "e799",
"unicode_decimal": 59289
},
{
"icon_id": "38884170",
"name": "icon_swagger",
"font_class": "icon_swagger",
"unicode": "e798",
"unicode_decimal": 59288
},
{
"icon_id": "38747707",
"name": "icon_file- json",
"font_class": "a-icon_file-json",
"unicode": "e797",
"unicode_decimal": 59287
},
{
"icon_id": "38499852",
"name": "icon_keyboard",

View File

@ -14,6 +14,14 @@
/>
<missing-glyph />
<glyph glyph-name="icon_carriage_return2" unicode="&#59290;" d="M311.168 499.498667a42.666667 42.666667 0 1 0 60.330667-60.330667L273.706667 341.333333H789.333333a21.333333 21.333333 0 0 1 21.333334 21.333334V640a42.666667 42.666667 0 0 0 85.333333 0v-277.333333a106.666667 106.666667 0 0 0-106.666667-106.666667H273.706667l97.792-97.834667a42.666667 42.666667 0 0 0 3.541333-56.32l-3.541333-4.010666a42.666667 42.666667 0 0 0-60.330667 0l-170.666667 170.666666-0.938666 0.938667a42.922667 42.922667 0 0 0-2.474667 2.901333l3.413333-3.84A43.008 43.008 0 0 0 128 297.813333V299.648c0 0.938667 0.085333 1.877333 0.170667 2.816L128 298.666667a43.008 43.008 0 0 0 9.088 26.325333c1.066667 1.322667 2.176 2.645333 3.413333 3.84l170.666667 170.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_carriage_return1" unicode="&#59289;" d="M896 725.333333a85.333333 85.333333 0 0 0 85.333333-85.333333v-512a85.333333 85.333333 0 0 0-85.333333-85.333333H336.810667a85.333333 85.333333 0 0 0-58.24 22.954666l-4.608 5.077334-222.378667 287.146666a42.666667 42.666667 0 0 0 0 52.266667l222.378667 287.189333 4.608 5.077334A85.333333 85.333333 0 0 0 336.810667 725.333333H896z m0-85.333333H337.493333l-198.229333-256 198.272-256H896V640z m-396.501333-119.168l76.501333-76.458667 76.501333 76.458667a42.666667 42.666667 0 0 0 60.330667-60.330667L636.373333 384l76.458667-76.501333a42.666667 42.666667 0 0 0-60.330667-60.330667L576 323.626667l-76.501333-76.458667a42.666667 42.666667 0 0 0-60.330667 60.330667L515.626667 384l-76.458667 76.501333a42.666667 42.666667 0 0 0 60.330667 60.330667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_swagger" unicode="&#59288;" d="M298.666667 853.333333a42.666667 42.666667 0 1 0 0-85.333333c-39.125333 0-64-18.688-64-64V512c0-42.624-24.533333-77.184-63.573334-106.496l-3.456-2.474667 3.2 2.304c36.693333-27.52 60.501333-59.477333 63.488-98.261333L234.666667 298.666667v-234.666667c0-45.354667 24.874667-64 64-64a42.666667 42.666667 0 0 0 0-85.333333c-81.792 0-149.333333 50.688-149.333334 149.333333V298.666667c0 10.538667-12.074667 26.24-35.114666 42.368a249.984 249.984 0 0 1-44.714667 24.704c-35.797333 14.293333-35.797333 64.896 0 79.189333a249.984 249.984 0 0 1 44.714667 24.661333c23.04 16.170667 35.114667 31.872 35.114666 42.410667V704C149.333333 802.688 216.874667 853.333333 298.666667 853.333333z m426.666666 0c81.792 0 149.333333-50.688 149.333334-149.333333V512c0-10.538667 12.074667-26.24 35.114666-42.368a249.984 249.984 0 0 1 44.714667-24.704c35.797333-14.293333 35.797333-64.896 0-79.189333a249.984 249.984 0 0 1-44.714667-24.661334c-23.04-16.170667-35.114667-31.872-35.114666-42.410666v-234.666667c0-98.688-67.541333-149.333333-149.333334-149.333333a42.666667 42.666667 0 0 0 0 85.333333c39.125333 0 64 18.688 64 64V298.666667c0 42.624 24.533333 77.184 63.573334 106.496-36.437333 27.733333-60.245333 59.648-63.232 98.432L789.333333 512V704c0 45.354667-24.874667 64-64 64a42.666667 42.666667 0 0 0 0 85.333333z m-362.666666-384a64 64 0 1 0 0-128 64 64 0 0 0 0 128z m170.666666 0a64 64 0 1 0 0-128 64 64 0 0 0 0 128z m170.666667 0a64 64 0 1 0 0-128 64 64 0 0 0 0 128z" horiz-adv-x="1024" />
<glyph glyph-name="a-icon_file-json" unicode="&#59287;" d="M648.533333 853.333333a42.666667 42.666667 0 0 0 33.322667-16l170.325333-212.906666 1.792-2.346667 0.213334-0.341333 0.426666-0.554667 0.426667-0.768-0.853333 1.322667-1.664 2.261333 2.730666-3.925333 1.152-1.877334a40.021333 40.021333 0 0 0 4.053334-9.941333 41.258667 41.258667 0 0 0 1.109333-5.973333L861.866667 597.333333v-85.333333h42.666666a85.333333 85.333333 0 0 0 85.333334-85.333333v-298.666667a85.333333 85.333333 0 0 0-85.333334-85.333333h-42.666666v-85.333334a42.666667 42.666667 0 0 0-42.666667-42.666666h-597.333333a42.666667 42.666667 0 0 0-42.666667 42.666666v85.333334h-42.666667a85.333333 85.333333 0 0 0-85.333333 85.333333v298.666667a85.333333 85.333333 0 0 0 85.333333 85.333333h42.666667V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h426.666666z m128-810.666666h-512v-42.666667h512v42.666667z m128 384h-768v-298.666667h768v298.666667z m-341.333333 341.333333h-298.666667v-256h512V554.666667h-170.666666a42.666667 42.666667 0 0 0-42.666667 42.666666V768z m85.333333-25.6V640h81.92L648.533333 742.4z m-341.333333-374.698667v-141.056c0-17.749333-3.157333-32.384-9.898667-43.733333-11.690667-19.797333-32.512-29.525333-60.629333-29.525333-27.818667 0-47.957333 7.850667-58.88 24.618666-9.642667 14.848-14.293333 34.346667-14.293333 58.282667v17.066667h61.952v-16.768c0.298667-12.544 1.621333-21.077333 3.413333-24.789334 0.426667-0.810667 1.706667-1.450667 6.4-1.450666 4.48 0 5.76 0.682667 6.4 2.133333 0.810667 1.621333 1.450667 6.4 1.450667 13.568v141.653333H307.2z m277.589333 5.632c29.568 0 53.034667-8.362667 68.992-24.832 21.504-19.584 32.042667-48.341333 32.042667-85.461333 0-36.394667-10.496-65.109333-31.488-84.906667-16.512-17.066667-39.978667-25.386667-69.546667-25.386666-29.568 0-53.034667 8.362667-69.12 24.96-14.08 13.226667-23.466667 30.165333-28.288 50.474666l0.213334-3.626666c0-20.565333-8.106667-38.101333-23.765334-51.626667-15.402667-13.354667-36.565333-19.797333-62.933333-19.797333-26.666667 0-48.341333 6.272-64.597333 19.2-16.853333 13.482667-25.429333 32.256-25.429334 55.210666v12.8h55.594667l-0.426667 0.085334a114.986667 114.986667 0 0 0-23.808 9.344l-4.181333 2.56c-16.085333 11.050667-24.192 28.373333-24.192 50.474666 0 20.053333 7.509333 37.205333 22.186667 50.56 14.634667 13.312 35.328 19.712 61.482666 19.712 22.101333 0 41.386667-5.973333 57.386667-18.005333 16.938667-12.714667 25.898667-31.274667 26.666667-54.613333l0.426666-13.226667H432.64l4.693333-1.237333c9.6-2.816 17.706667-6.272 24.277334-10.453334l4.650666-3.328c8.917333-7.04 15.061333-16.042667 18.346667-26.752a144 144 0 0 0-0.938667 17.578667c0 37.12 10.581333 65.92 31.573334 84.906667 16.554667 17.066667 40.021333 25.386667 69.589333 25.386666zM371.754667 238.933333l1.322666-9.642666c0.853333-6.272 2.389333-10.410667 4.096-12.501334 3.456-4.181333 10.538667-6.698667 22.229334-6.698666 7.765333 0 13.738667 0.853333 17.834666 2.261333 5.802667 2.048 7.637333 4.48 7.637334 9.301333 0 1.621333-0.384 2.261333-1.877334 3.2-3.882667 2.389333-11.050667 4.821333-21.248 7.082667l-19.541333 4.352a319.744 319.744 0 0 0-8.448 2.048l-2.005333 0.554667z m213.034666 76.202667c-11.434667 0-19.925333-3.925333-26.538666-12.117333-7.04-8.746667-10.794667-21.930667-10.794667-39.978667 0-18.048 3.754667-31.232 10.794667-39.978667 6.613333-8.192 15.104-12.074667 26.538666-12.074666s19.84 3.882667 26.368 12.032l2.730667 4.010666c5.12 8.533333 7.850667 20.48 7.850667 36.010667 0 18.005333-3.712 31.189333-10.666667 39.978667-6.485333 8.192-14.890667 12.117333-26.282667 12.117333z m-189.866666 0.341333c-7.552 0-13.013333-1.365333-16.597334-3.797333-2.389333-1.621333-3.242667-3.2-3.242666-6.144 0-1.92 0.426667-2.56 2.346666-3.669333 2.346667-1.322667 9.514667-3.626667 20.608-6.272l21.461334-5.12-0.469334 8.789333c-0.341333 5.376-1.877333 8.874667-4.992 11.349333l-2.56 1.706667a36.309333 36.309333 0 0 1-16.554666 3.157333z m354.688 52.224l3.669333-6.442666 49.493333-86.741334v93.184h61.44V157.866667H805.546667l-3.669334 6.4-51.413333 89.344 0.042667-95.744h-61.44v209.834666h60.586666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_keyboard" unicode="&#59286;" d="M853.333333 810.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-109.354667a64 64 0 0 0-64-64l-85.333333 0.042667V554.666667H896a85.333333 85.333333 0 0 0 85.333333-85.333334v-426.666666a85.333333 85.333333 0 0 0-85.333333-85.333334H128a85.333333 85.333333 0 0 0-85.333333 85.333334V469.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h533.333333V616.021333a64 64 0 0 0 64 64h85.333334V768a42.666667 42.666667 0 0 0 37.674666 42.368L853.333333 810.666667z m42.666667-341.333334H128v-426.666666h768V469.333333z m-234.666667-298.666666a42.666667 42.666667 0 0 0 0-85.333334h-298.666666a42.666667 42.666667 0 0 0 0 85.333334h298.666666zM341.333333 298.666667a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m-128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m256 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334zM298.666667 426.666667a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m128 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z" horiz-adv-x="1024" />
<glyph glyph-name="icon_split-turn-down-left" unicode="&#59285;" d="M746.666667 853.333333a149.333333 149.333333 0 0 0 42.666666-292.48v-501.674666l33.834667 33.792a42.666667 42.666667 0 0 0 56.32 3.541333l4.010667-3.541333a42.666667 42.666667 0 0 0 0-60.330667l-106.666667-106.666667a42.666667 42.666667 0 0 0-60.330667 0l-106.666666 106.666667a42.666667 42.666667 0 0 0 60.330666 60.330667l33.834667-33.834667v323.669333h-298.666667a128 128 0 0 1-128-128v-195.626666l33.834667 33.792a42.666667 42.666667 0 0 0 56.32 3.541333l4.010667-3.541333a42.666667 42.666667 0 0 0 0-60.330667l-106.666667-106.666667a42.922667 42.922667 0 0 0-3.84-3.413333l3.84 3.413333a43.008 43.008 0 0 0-28.757333-12.501333h-2.773334c-0.768 0-1.536 0.085333-2.304 0.170667l3.669334-0.170667a43.008 43.008 0 0 0-26.325334 9.088 43.349333 43.349333 0 0 0-3.84 3.413333l-106.666666 106.666667a42.666667 42.666667 0 0 0 60.330666 60.330667l33.834667-33.834667v195.669333a213.333333 213.333333 0 0 0 213.333333 213.333334h298.666667V560.853333A149.418667 149.418667 0 0 0 746.666667 853.333333z m0-85.333333a64 64 0 1 1 0-128 64 64 0 0 1 0 128z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 427 KiB

View File

@ -177,6 +177,12 @@
.btn-text-sec-active();
.btn-text-sec-disabled();
}
.arco-btn-text--secondary {
color: var(--color-text-3) !important;
.btn-text-sec-hover();
.btn-text-sec-active();
.btn-text-sec-disabled();
}
.arco-btn-text--danger {
color: rgb(var(--danger-6)) !important;
.btn-text-danger-hover();

View File

@ -211,7 +211,7 @@
@select="selectAutoComplete"
>
<template #suffix>
<MsIcon type="icon-icon_mock" class="ms-params-input-mock-icon" @click.stop="openParamSetting" />
<MsIcon type="icon-icon_mock" class="ms-params-input-suffix-icon" @click.stop="openParamSetting" />
</template>
<template #option="{ data }">
<div class="w-[350px]">
@ -588,17 +588,23 @@
}
}
.ms-params-input {
.ms-params-input-mock-icon {
.ms-params-input-suffix-icon,
.ms-params-input-suffix-icon--disabled {
@apply invisible;
}
&:hover,
&.arco-input-focus {
.ms-params-input-mock-icon {
.ms-params-input-suffix-icon {
@apply visible cursor-pointer;
&:hover {
color: rgb(var(--primary-5));
}
}
.ms-params-input-suffix-icon--disabled {
@apply visible cursor-not-allowed;
color: rgb(var(--primary-3));
}
}
:deep(.arco-select-option) {
@apply flex flex-1 p-10;
@ -606,11 +612,16 @@
}
.ms-params-input--focus {
border-color: rgb(var(--primary-5)) !important;
.ms-params-input-mock-icon {
.ms-params-input-suffix-icon {
@apply visible cursor-pointer;
color: rgb(var(--primary-5));
}
.ms-params-input-suffix-icon--disabled {
@apply visible cursor-not-allowed;
color: rgb(var(--primary-3));
}
}
.ms-params-input-trigger {
width: 350px;

View File

@ -1,90 +0,0 @@
<template>
<pre ref="jr" :class="props.class" @click="pickPath"></pre>
<input ref="ip" :value="jsonPath" class="path" type="hidden" />
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, Ref, ref } from 'vue';
import JPPicker from '@/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla';
import { Recordable } from '#/global';
const jr: Ref<HTMLElement | null> = ref(null);
const ip: Ref<HTMLInputElement | null> = ref(null);
const jsonPath = ref('');
const props = withDefaults(
defineProps<{
data: object;
opt?: Recordable<any>;
class?: string;
}>(),
{
data: () => ({
users: [
{
id: 1,
name: 'John',
age: 'Number(25.0000000000000000000)',
},
{
id: 2,
name: 'Jane',
age: 30,
},
{
id: 3,
name: 'Mike',
age: 28,
},
],
products: [
{
id: 101,
name: 'iPhone',
price: 999,
},
{
id: 102,
name: 'MacBook',
price: 1599,
},
{
id: 103,
name: 'iPad',
price: 799,
},
],
}),
opt: () => ({}),
}
);
const emit = defineEmits<{
(e: 'pick', path: string): void;
}>();
onMounted(() => {
JPPicker.jsonPathPicker(jr.value, props.data, [ip.value], props.opt);
});
function pickPath(ev: any) {
if (ev.target && ev.target.classList.contains('pick-path')) {
setTimeout(() => {
if (ip.value) {
jsonPath.value = ip.value.value;
emit('pick', jsonPath.value);
}
}, 0);
}
}
onUnmounted(() => {
JPPicker.clearJsonPathPicker(jr.value);
});
</script>
<style lang="css">
@import url('@/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css');
</style>

View File

@ -1,24 +1,39 @@
<template>
<div ref="fullRef" class="h-full rounded-[4px] bg-[var(--color-fill-1)] p-[12px]">
<div v-if="showTitleLine" class="mb-[12px] flex justify-between pr-[12px]">
<slot name="title">
<span class="font-medium">{{ title }}</span>
</slot>
<div v-if="showThemeChange">
<div v-if="showTitleLine" class="mb-[12px] flex items-center justify-between pr-[12px]">
<div>
<a-select
v-if="showLanguageChange"
v-model:model-value="currentLanguage"
:options="languageOptions"
class="mr-[4px] w-[100px]"
size="small"
@change="(val) => handleLanguageChange(val as Language)"
/>
<a-select
v-if="showThemeChange"
v-model:model-value="currentTheme"
:options="themeOptions"
class="w-[100px]"
size="small"
@change="(val) => handleThemeChange(val as Theme)"
></a-select>
/>
</div>
<div v-if="showFullScreen" class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]" @click="toggle">
<div>
<slot name="title">
<span class="font-medium">{{ title }}</span>
</slot>
<div
v-if="showFullScreen"
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="toggle"
>
<MsIcon v-if="isFullscreen" type="icon-icon_minify_outlined" />
<MsIcon v-else type="icon-icon_magnify_outlined" />
{{ t('msCodeEditor.fullScreen') }}
</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>
@ -35,20 +50,23 @@
import './userWorker';
import MsCodeEditorTheme from './themes';
import { CustomTheme, editorProps, Theme } from './types';
import { CustomTheme, editorProps, Language, LanguageEnum, Theme } from './types';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
export default defineComponent({
name: 'MonacoEditor',
props: editorProps,
emits: ['update:modelValue', 'change', 'editorMounted'],
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
const { t } = useI18n();
let editor: monaco.editor.IStandaloneCodeEditor;
const codeEditBox = ref();
// ref
const fullRef = ref<HTMLElement | null>();
//
const currentTheme = ref<Theme>(props.theme);
//
const themeOptions = [
{ label: 'vs', value: 'vs' },
{ label: 'vs-dark', value: 'vs-dark' },
@ -59,7 +77,32 @@
value: item,
}))
);
const showTitleLine = computed(() => props.title || props.showThemeChange || props.showFullScreen);
//
const currentLanguage = ref<Language>(props.language);
//
const languageOptions = Object.values(LanguageEnum)
.map((e) => {
if (props.languages) {
//
if (props.languages.includes(e)) {
return {
label: e,
value: e,
};
}
return false;
}
return {
label: e,
value: e,
};
})
.filter(Boolean) as { label: string; value: Language }[];
//
const showTitleLine = computed(
() => props.title || props.showThemeChange || props.showLanguageChange || props.showFullScreen
);
watch(
() => props.theme,
@ -72,6 +115,10 @@
monaco.editor.setTheme(val);
}
function handleLanguageChange(val: Language) {
monaco.editor.setModelLanguage(editor.getModel()!, val);
}
const init = () => {
//
Object.keys(MsCodeEditorTheme).forEach((e) => {
@ -180,10 +227,13 @@
isFullscreen,
currentTheme,
themeOptions,
currentLanguage,
languageOptions,
showTitleLine,
toggle,
t,
handleThemeChange,
handleLanguageChange,
insertContent,
undo,
redo,

View File

@ -4,21 +4,23 @@ export type CustomTheme = 'MS-text';
export type Theme = 'vs' | 'hc-black' | 'vs-dark' | CustomTheme;
export type FoldingStrategy = 'auto' | 'indentation';
export type RenderLineHighlight = 'all' | 'line' | 'none' | 'gutter';
export type Language =
| 'plaintext'
| 'javascript'
| 'typescript'
| 'css'
| 'less'
| 'sass'
| 'html'
| 'sql'
| 'json'
| 'java'
| 'python'
| 'xml'
| 'yaml'
| 'shell';
export const LanguageEnum = {
PLAINTEXT: 'plaintext' as const,
JAVASCRIPT: 'javascript' as const,
TYPESCRIPT: 'typescript' as const,
CSS: 'css' as const,
LESS: 'less' as const,
SASS: 'sass' as const,
HTML: 'html' as const,
SQL: 'sql' as const,
JSON: 'json' as const,
JAVA: 'java' as const,
PYTHON: 'python' as const,
XML: 'xml' as const,
YAML: 'yaml' as const,
SHELL: 'shell' as const,
} as const;
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum];
export interface Options {
automaticLayout: boolean; // 自适应布局
foldingStrategy: FoldingStrategy; // 折叠方式 auto | indentation
@ -87,6 +89,15 @@ export const editorProps = {
type: Boolean as PropType<boolean>,
default: true,
},
languages: {
// 支持选择的语言种类
type: Array as PropType<Array<Language>>,
default: undefined,
},
showLanguageChange: {
type: Boolean as PropType<boolean>,
default: false,
},
showThemeChange: {
type: Boolean as PropType<boolean>,
default: true,

View File

@ -0,0 +1,98 @@
<template>
<pre ref="jr" :class="['container', props.class]" @click="pickPath"></pre>
<input ref="ip" :value="jsonPath" class="path" type="hidden" />
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { JSONPath } from 'jsonpath-plus';
import JPPicker from '@/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla';
import { Recordable } from '#/global';
const jr: Ref<HTMLElement | null> = ref(null);
const ip: Ref<HTMLInputElement | null> = ref(null);
const json = ref<string | Recordable<any>>('');
const jsonPath = ref('');
const props = withDefaults(
defineProps<{
data: string | Recordable<any>;
opt?: Recordable<any>;
class?: string;
}>(),
{
opt: () => ({}),
}
);
const emit = defineEmits<{
(e: 'pick', path: string, result: any[]): void;
}>();
function initJsonPathPicker() {
try {
json.value = props.data;
if (typeof props.data === 'string') {
json.value = JSON.parse(props.data);
}
JPPicker.jsonPathPicker(jr.value, json.value, [ip.value], props.opt);
} catch (error) {
JPPicker.jsonPathPicker(jr.value, props.data, [ip.value], props.opt);
}
}
onMounted(() => {
initJsonPathPicker();
});
watch(
() => props.data,
() => {
initJsonPathPicker();
},
{
deep: true,
}
);
function pickPath(ev: any) {
if (ev.target && ev.target.classList.contains('pick-path')) {
setTimeout(() => {
if (ip.value) {
jsonPath.value = ip.value.value;
emit('pick', jsonPath.value, JSONPath({ json: json.value, path: jsonPath.value }));
}
}, 0);
}
}
onUnmounted(() => {
JPPicker.clearJsonPathPicker(jr.value);
});
</script>
<style>
@import url('@/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css');
</style>
<style lang="less" scoped>
.container {
@apply h-full overflow-y-auto;
.ms-scroll-bar();
padding: 12px 1.85em;
border-radius: var(--border-radius-small);
:deep(.json-string) {
color: rgb(var(--link-7));
}
:deep(.json-literal) {
color: rgb(var(--primary-4));
}
:deep(.json-toggle),
:deep(.json-dict > li) {
color: var(--color-text-2);
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div v-if="parsedXml">
<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>
@ -8,6 +8,7 @@
<script setup lang="ts">
import { XpathNode } from './types';
import * as XmlBeautify from 'xml-beautify';
const props = defineProps<{
xmlString: string;
@ -67,7 +68,6 @@
emit('pick', xpath);
}
}
/**
* 解析xml
*/
@ -76,12 +76,12 @@
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
parsedXml.value = xmlDoc;
// XML icon
flattenedXml.value = props.xmlString
// XML icon
flattenedXml.value = new XmlBeautify({ parser: DOMParser })
.beautify(props.xmlString)
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/(&lt;\w+\s*[^&gt;]*&gt;)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
.replace(/(&lt;\/\w+\s*[^&gt;]*&gt;)/g, '<span style="color: rgb(var(--primary-5));">$1</span>')
.replace(/(&lt;([^/][^&]*?)&gt;)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
.split(/\r?\n/)
.map((e) => ({ content: e, xpath: '' }));
// XML xpath
@ -115,3 +115,13 @@
}
);
</script>
<style lang="less" scoped>
.container {
@apply h-full overflow-y-auto;
.ms-scroll-bar();
padding: 12px 1.85em;
border-radius: var(--border-radius-small);
}
</style>

View File

@ -16,7 +16,7 @@
<div
:class="`ms-split-box ${props.direction === 'horizontal' ? 'ms-split-box--left' : 'ms-split-box--top'} ${
props.disabled && props.direction === 'horizontal' ? 'border-r border-[var(--color-text-n8)]' : ''
}`"
} ${props.firstContainerClass}`"
>
<div
v-if="props.direction === 'horizontal' && props.expandDirection === 'right' && !props.disabled"
@ -51,7 +51,11 @@
/>
</div>
</div>
<div :class="`ms-split-box ${props.direction === 'horizontal' ? 'ms-split-box--right' : 'ms-split-box--bottom'}`">
<div
:class="`ms-split-box ${props.direction === 'horizontal' ? 'ms-split-box--right' : 'ms-split-box--bottom'} ${
props.secondContainerClass
}`"
>
<slot name="second"></slot>
</div>
</template>
@ -71,6 +75,8 @@
direction?: 'horizontal' | 'vertical';
expandDirection?: 'left' | 'right' | 'top'; // TODO: bottom left top
disabled?: boolean; //
firstContainerClass?: string; // first
secondContainerClass?: string; // second
}>(),
{
size: '300px',

View File

@ -17,9 +17,10 @@
<slot name="optional" v-bind="{ rowIndex, record }" />
</template>
<template #columns>
<a-table-column v-if="attrs.selectable && props.selectedKeys" :width="60">
<a-table-column v-if="attrs.selectable && props.selectedKeys" :width="props.firstColumnWidth || 60">
<template #title>
<SelectALL
v-if="attrs.selectorType === 'checkbox'"
:total="selectTotal"
:current="selectCurrent"
:show-select-all="(attrs.showPagination as boolean) && props.showSelectorAll"
@ -29,9 +30,15 @@
</template>
<template #cell="{ record }">
<MsCheckbox
v-if="attrs.selectorType === 'checkbox'"
:value="props.selectedKeys.has(record[rowKey || 'id'])"
@change="rowSelectChange(record[rowKey || 'id'])"
/>
<a-radio
v-else-if="attrs.selectorType === 'radio'"
v-model:model-value="innerSelectedKey"
:value="record[rowKey || 'id']"
/>
</template>
</a-table-column>
<a-table-column
@ -54,7 +61,7 @@
:tooltip="item.tooltip"
>
<template #title>
<div class="flex w-full flex-row flex-nowrap items-center">
<div :class="{ 'flex w-full flex-row flex-nowrap items-center': !item.align }">
<slot :name="item.titleSlotName" :column-config="item">
<div class="text-[var(--color-text-3)]">{{ t(item.title as string) }}</div>
</slot>
@ -80,7 +87,7 @@
</div>
</template>
<template #cell="{ column, record, rowIndex }">
<div :class="{ 'flex flex-row items-center': !item.isTag }">
<div :class="{ 'flex flex-row items-center': !item.isTag && !item.align }">
<template v-if="item.dataIndex === SpecialColumnEnum.ENABLE">
<slot name="enable" v-bind="{ record }">
<div v-if="record.enable" class="flex flex-row flex-nowrap items-center gap-[2px]">
@ -224,6 +231,7 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, useAttrs } from 'vue';
import { useVModel } from '@vueuse/core';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPagination from '@/components/pure/ms-pagination/index';
@ -259,6 +267,7 @@
const props = defineProps<{
selectedKeys: Set<string>;
selectedKey: string;
excludeKeys: Set<string>;
selectorStatus: SelectAllEnum;
actionConfig?: BatchActionConfig;
@ -270,8 +279,10 @@
rowClass?: string | any[] | Record<string, any> | ((record: TableData, rowIndex: number) => any);
spanAll?: boolean;
showSelectorAll?: boolean;
firstColumnWidth?: number; //
}>();
const emit = defineEmits<{
(e: 'update:selectedKey', value: string): void;
(e: 'batchAction', value: BatchActionParams, queryParams: BatchActionQueryParams): void;
(e: 'pageChange', value: number): void;
(e: 'pageSizeChange', value: number): void;
@ -357,6 +368,8 @@
}
};
const innerSelectedKey = useVModel(props, 'selectedKey', emit); //
// change
const handleSelectAllChange = (v: SelectAllEnum) => {
emit('selectAllChange', v);
@ -512,6 +525,11 @@
color: rgb(var(--primary-7));
opacity: 0;
}
:deep(.arco-table-cell-align-left) {
.arco-table-td-content {
@apply flex items-center;
}
}
:deep(.arco-table-hover) {
:not(.arco-table-dragging) {
.arco-table-tr:not(.arco-table-tr-empty):not(.arco-table-tr-summary):hover {

View File

@ -82,7 +82,8 @@ export interface MsTableProps<T> {
/** 选择器相关 */
selectable?: boolean; // 是否显示选择器
selectorType: 'none' | 'checkbox' | 'radio'; // 选择器类型
selectedKeys: Set<string>; // 选中的key
selectedKeys: Set<string>; // 选中的key多选
selectedKey: string; // 选中的key单选
excludeKeys: Set<string>; // 排除的key
selectorStatus: SelectAllEnum; // 选择器状态
showSelectorAll?: boolean; // 是否显示跨页全选选择器

View File

@ -59,7 +59,8 @@ export default function useTableProps<T>(
rowSelection: null, // 禁用表格默认的选择器
selectable: false, // 是否显示选择器
selectorType: 'checkbox', // 选择器类型
selectedKeys: new Set<string>(), // 选中的key
selectedKeys: new Set<string>(), // 选中的key, 多选
selectedKey: '', // 选中的key单选
excludeKeys: new Set<string>(), // 排除的key
selectorStatus: SelectAllEnum.NONE, // 选择器状态
showSelectorAll: true, // 是否显示全选

View File

@ -1,81 +1,12 @@
import { ReviewItem } from '@/models/caseManagement/caseReview';
import { ConditionType } from '@/models/apiTest/debug';
// 评审结果
export const reviewResultMap = {
UN_REVIEWED: {
label: 'caseManagement.caseReview.unReview',
color: 'var(--color-text-input-border)',
icon: 'icon-icon_block_filled',
},
UNDER_REVIEWED: {
label: 'caseManagement.caseReview.reviewing',
color: 'rgb(var(--link-6))',
icon: 'icon-icon_testing',
},
PASS: {
label: 'caseManagement.caseReview.reviewPass',
color: 'rgb(var(--success-6))',
icon: 'icon-icon_succeed_filled',
},
UN_PASS: {
label: 'caseManagement.caseReview.fail',
color: 'rgb(var(--danger-6))',
icon: 'icon-icon_close_filled',
},
RE_REVIEWED: {
label: 'caseManagement.caseReview.reReview',
color: 'rgb(var(--warning-6))',
icon: 'icon-icon_resubmit_filled',
},
} as const;
// 评审状态
export const reviewStatusMap = {
PREPARED: {
label: 'caseManagement.caseReview.unStart',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-1)]',
},
UNDERWAY: {
label: 'caseManagement.caseReview.going',
color: 'rgb(var(--link-2))',
class: '!text-[rgb(var(--link-6))]',
},
COMPLETED: {
label: 'caseManagement.caseReview.finished',
color: 'rgb(var(--success-2))',
class: '!text-[rgb(var(--success-6))]',
},
ARCHIVED: {
label: 'caseManagement.caseReview.archived',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-4)]',
},
} as const;
// 评审详情
export const reviewDefaultDetail: ReviewItem = {
id: '',
num: 0,
moduleId: '',
projectId: '',
reviewPassRule: 'SINGLE',
name: '',
status: 'PREPARED',
caseCount: 0,
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
pos: 5000,
startTime: 0,
endTime: 0,
passRate: 0,
tags: [],
description: '',
createTime: 0,
createUser: '',
updateTime: 0,
updateUser: '',
reviewers: [],
reReviewedCount: 0,
followFlag: false,
// 条件操作类型
export type ConditionTypeNameMap = Record<ConditionType, string>;
export const conditionTypeNameMap = {
script: 'apiTestDebug.script',
sql: 'apiTestDebug.sql',
waitTime: 'apiTestDebug.waitTime',
extract: 'apiTestDebug.extractParameter',
};
export default {};

View File

@ -0,0 +1,97 @@
import { ReviewItem, ReviewResult, ReviewStatus } from '@/models/caseManagement/caseReview';
// 评审结果
export type ReviewResultMap = Record<
ReviewResult,
{
label: string;
color: string;
icon: string;
}
>;
export const reviewResultMap: ReviewResultMap = {
UN_REVIEWED: {
label: 'caseManagement.caseReview.unReview',
color: 'var(--color-text-input-border)',
icon: 'icon-icon_block_filled',
},
UNDER_REVIEWED: {
label: 'caseManagement.caseReview.reviewing',
color: 'rgb(var(--link-6))',
icon: 'icon-icon_testing',
},
PASS: {
label: 'caseManagement.caseReview.reviewPass',
color: 'rgb(var(--success-6))',
icon: 'icon-icon_succeed_filled',
},
UN_PASS: {
label: 'caseManagement.caseReview.fail',
color: 'rgb(var(--danger-6))',
icon: 'icon-icon_close_filled',
},
RE_REVIEWED: {
label: 'caseManagement.caseReview.reReview',
color: 'rgb(var(--warning-6))',
icon: 'icon-icon_resubmit_filled',
},
};
// 评审状态
export type ReviewStatusMap = Record<
ReviewStatus,
{
label: string;
color: string;
class: string;
}
>;
export const reviewStatusMap: ReviewStatusMap = {
PREPARED: {
label: 'caseManagement.caseReview.unStart',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-1)]',
},
UNDERWAY: {
label: 'caseManagement.caseReview.going',
color: 'rgb(var(--link-2))',
class: '!text-[rgb(var(--link-6))]',
},
COMPLETED: {
label: 'caseManagement.caseReview.finished',
color: 'rgb(var(--success-2))',
class: '!text-[rgb(var(--success-6))]',
},
ARCHIVED: {
label: 'caseManagement.caseReview.archived',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-4)]',
},
};
// 评审详情
export const reviewDefaultDetail: ReviewItem = {
id: '',
num: 0,
moduleId: '',
projectId: '',
reviewPassRule: 'SINGLE',
name: '',
status: 'PREPARED' as ReviewStatus,
caseCount: 0,
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
pos: 5000,
startTime: 0,
endTime: 0,
passRate: 0,
tags: [],
description: '',
createTime: 0,
createUser: '',
updateTime: 0,
updateUser: '',
reviewers: [],
reReviewedCount: 0,
followFlag: false,
};

View File

@ -15,7 +15,7 @@ export enum RequestComposition {
BODY = 'BODY',
QUERY = 'QUERY',
REST = 'REST',
PREFIX = 'PREFIX',
PRECONDITION = 'PRECONDITION',
POST_CONDITION = 'POST_CONDITION',
ASSERTION = 'ASSERTION',
AUTH = 'AUTH',

View File

@ -1,21 +0,0 @@
export interface APIListItemI {
id: number;
type: string;
receiver: string;
title: string;
status: string;
createTime: number | string;
operator: string;
operation: string;
resourceId: string;
resourceType: string;
resourceName: string;
content: string;
}
export interface SortItem {
[key: string]: string;
}
export interface FilterItem {
[key: string]: any;
}

View File

@ -0,0 +1,13 @@
// 条件操作类型
export type ConditionType = 'script' | 'sql' | 'waitTime' | 'extract';
// 表达式类型
export type ExpressionType = 'regular' | 'JSONPath' | 'XPath';
// 表达式配置
export interface ExpressionConfig {
expression: string;
expressionType?: ExpressionType;
regexpMatchRule?: 'expression' | 'group'; // 正则表达式匹配规则
resultMatchRule?: 'random' | 'specify' | 'all'; // 结果匹配规则
specifyMatchNum?: number; // 指定匹配下标
xmlMatchContentType?: 'xml' | 'html'; // 响应内容格式
}

View File

@ -0,0 +1,27 @@
import { DOMParser } from '@xmldom/xmldom';
import * as xpath from 'xpath';
/**
* xpath xml
* @param xmlText xml文本
* @param xpathQuery xpath
* @returns
*/
export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.SelectReturnType {
try {
// 解析 XML 文本
const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml');
// 使用 XPath 查询匹配的节点
const nodes = xpath.select(xpathQuery, xmlDoc);
// 返回匹配结果
return nodes;
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error parsing XML or executing XPath query:', error);
return null;
}
}
export default { matchXMLWithXPath };

View File

@ -0,0 +1,703 @@
<template>
<div class="condition-content">
<!-- 脚本操作 -->
<template v-if="condition.type === 'script'">
<a-radio-group v-model:model-value="condition.scriptType" size="small" class="mb-[16px]">
<a-radio value="manual">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio value="quote">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<div
v-if="condition.scriptType === 'manual'"
class="relative rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
<a-input
ref="scriptNameInputRef"
v-model:model-value="condition.name"
:placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')"
:max-length="255"
show-word-limit
size="small"
@press-enter="isShowEditScriptNameInput = false"
@blur="isShowEditScriptNameInput = false"
/>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<a-tooltip :content="condition.name">
<div class="script-name-container">
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ condition.name }}
</div>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
</div>
</a-tooltip>
<a-popover class="h-auto" position="top">
<div class="text-[rgb(var(--primary-5))]">{{ t('apiTestDebug.scriptEx') }}</div>
<template #content>
<div class="mb-[8px] flex items-center justify-between">
<div class="text-[14px] font-medium text-[var(--color-text-1)]">
{{ t('apiTestDebug.scriptEx') }}
</div>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScriptEx"
>
{{ t('common.copy') }}
</a-button>
</div>
<div class="flex h-[412px]">
<MsCodeEditor
v-model:model-value="scriptEx"
class="flex-1"
theme="MS-text"
width="500px"
height="388px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
</template>
</a-popover>
</div>
<div class="flex items-center gap-[8px]">
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini">
<template #icon>
<MsIcon type="icon-icon_undo_outlined" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.revoke') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="clearScript">
<template #icon>
<MsIcon type="icon-icon_clear" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.clear') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyCondition">
{{ t('common.copy') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="deleteCondition">
{{ t('common.delete') }}
</a-button>
</div>
</div>
</div>
<div v-else class="flex h-[calc(100%-47px)] flex-col">
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ condition.quoteScript.name || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium">
{{ t('apiTestDebug.quote') }}
</MsButton>
</div>
<a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit">
<a-radio value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio>
<a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio>
</a-radio-group>
<MsBaseTable v-show="commonScriptShowType === 'parameters'" v-bind="propsRes" v-on="propsEvent">
<template #value="{ record }">
<a-tooltip :content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')">
<div
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
>
<div>*</div>
</div>
</a-tooltip>
{{ record.type }}
</template>
</MsBaseTable>
<div v-show="commonScriptShowType === 'scriptContent'" class="h-[calc(100%-76px)]">
<MsCodeEditor
v-model:model-value="condition.quoteScript.script"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
</div>
</template>
<!-- SQL操作 -->
<template v-else-if="condition.type === 'sql'">
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
<a-input
v-model:model-value="condition.desc"
:placeholder="t('apiTestDebug.commonPlaceholder')"
:max-length="255"
show-word-limit
/>
</div>
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ condition.sqlSource.name || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="quoteSqlSourceDrawerVisible = true">
{{ t('apiTestDebug.introduceSource') }}
</MsButton>
</div>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.sqlScript') }}</div>
<div class="mb-[16px] h-[300px]">
<MsCodeEditor
v-model:model-value="condition.sqlSource.script"
theme="vs"
height="276px"
:language="LanguageEnum.SQL"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
<div class="mb-[16px]">
<div class="mb-[8px] flex items-center text-[var(--color-text-1)]">
{{ t('apiTestDebug.storageType') }}
<a-tooltip position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div>{{ t('apiTestDebug.storageTypeTip1') }}</div>
<div>{{ t('apiTestDebug.storageTypeTip2') }}</div>
</template>
</a-tooltip>
</div>
<a-radio-group v-model:model-value="condition.sqlSource.storageType" size="small" type="button" class="w-fit">
<a-radio value="column">{{ t('apiTestDebug.storageByCol') }}</a-radio>
<a-radio value="result">{{ t('apiTestDebug.storageByResult') }}</a-radio>
</a-radio-group>
</div>
<div v-if="condition.sqlSource.storageType === 'column'" class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByCol') }}</div>
<a-input
v-model:model-value="condition.sqlSource.storageByCol"
:placeholder="t('apiTestDebug.storageByColPlaceholder', { a: '{id_1}', b: '{username_1}' })"
/>
</div>
<div v-else class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div>
<a-input
v-model:model-value="condition.sqlSource.storageByResult"
:placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })"
/>
</div>
<div v-if="condition.sqlSource.storageType === 'column'" class="sql-table-container">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.extractParameter') }}</div>
<paramTable
v-model:params="condition.sqlSource.params"
:columns="sqlSourceColumns"
:selectable="false"
@change="handleSqlSourceParamTableChange"
/>
</div>
</template>
<!-- 等待时间 -->
<div v-else-if="condition.type === 'waitTime'">
<div class="mb-[8px] flex items-center">
{{ t('apiTestDebug.waitTime') }}
<div class="text-[var(--color-text-4)]">(ms)</div>
</div>
<a-input-number v-model:model-value="condition.time" mode="button" :step="100" :min="0" class="w-[160px]" />
</div>
<!-- 提取参数 -->
<div v-else-if="condition.type === 'extract'">
<paramTable
ref="extractParamsTableRef"
v-model:params="condition.extractParams"
:default-param-item="defaultExtractParamItem"
:columns="extractParamsColumns"
:selectable="false"
:scroll="{ x: '700px' }"
:response="props.response"
:height-used="(props.heightUsed || 0) + 62"
@change="handleExtractParamTableChange"
@more-action-select="handleExtractParamMoreActionSelect"
>
<template #expression="{ record }">
<a-popover
position="tl"
:disabled="!record.expression || record.expression.trim() === ''"
class="ms-params-input-popover"
>
<template #content>
<div class="param-popover-title">
{{ t('apiTestDebug.expression') }}
</div>
<div class="param-popover-value">
{{ record.expression }}
</div>
</template>
<a-input
v-model:model-value="record.expression"
class="ms-params-input"
@input="handleExpressionChange"
@change="handleExpressionChange"
>
<template #suffix>
<a-tooltip :disabled="!disabledExpressionSuffix">
<template #content>
<div>{{ t('apiTestDebug.expressionTip1') }}</div>
<div>{{ t('apiTestDebug.expressionTip2') }}</div>
<div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template>
<MsIcon
type="icon-icon_flashlamp"
:size="15"
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record)"
/>
</a-tooltip>
</template>
</a-input>
</a-popover>
</template>
<template #operationPre="{ record }">
<a-popover
v-model:popupVisible="record.moreSettingPopoverVisible"
position="tl"
trigger="click"
:title="t('common.setting')"
:content-style="{ width: '480px' }"
>
<template #content>
<moreSetting v-model:config="activeRecord" is-popover class="mt-[12px]" />
<div class="flex items-center justify-end gap-[8px]">
<a-button type="secondary" size="mini" @click="record.moreSettingPopoverVisible = false">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" size="mini" @click="() => applyMoreSetting(record)">
{{ t('common.confirm') }}
</a-button>
</div>
</template>
<span class="invisible relative"></span>
</a-popover>
</template>
</paramTable>
</div>
</div>
<quoteSqlSourceDrawer v-model:visible="quoteSqlSourceDrawerVisible" @apply="handleQuoteSqlSourceApply" />
<fastExtraction
v-model:visible="fastExtractionVisible"
:response="props.response"
:config="activeRecord"
@apply="handleFastExtractionApply"
/>
</template>
<script setup lang="ts">
import { useClipboard, useVModel } from '@vueuse/core';
import { InputInstance, Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import fastExtraction from '../fastExtraction/index.vue';
import moreSetting from '../fastExtraction/moreSetting.vue';
import paramTable, { type ParamTableColumn } from '../paramTable.vue';
import quoteSqlSourceDrawer from '../quoteSqlSourceDrawer.vue';
import { useI18n } from '@/hooks/useI18n';
import { ExpressionConfig } from '@/models/apiTest/debug';
const props = defineProps<{
data: Record<string, any>;
response?: string; //
heightUsed?: number;
}>();
const emit = defineEmits<{
(e: 'update:data', data: Record<string, any>): void;
(e: 'copy'): void;
(e: 'delete', id: string): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
const condition = useVModel(props, 'data', emit);
//
const isShowEditScriptNameInput = ref(false);
const scriptNameInputRef = ref<InputInstance>();
function showEditScriptNameInput() {
isShowEditScriptNameInput.value = true;
nextTick(() => {
scriptNameInputRef.value?.focus();
});
}
const scriptEx = ref(`2023-12-04 11:19:28 INFO 9026fd6a 1-1 Thread started: 9026fd6a 1-1
2023-12-04 11:19:28 ERROR 9026fd6a 1-1 Problem in JSR223 script JSR223Sampler, message: {}
In file: inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' Encountered "import" at line 2, column 1.
in inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException
org.apache.http.client.method . . . '' at line number 2
`);
const { copy, isSupported } = useClipboard();
function copyScriptEx() {
if (isSupported) {
copy(scriptEx.value);
Message.success(t('apiTestDebug.scriptExCopySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
function clearScript() {
condition.value.script = '';
}
/**
* 复制条件
*/
function copyCondition() {
emit('copy');
}
/**
* 删除条件
*/
function deleteCondition() {
emit('delete', condition.value.id);
}
const commonScriptShowType = ref<'parameters' | 'scriptContent'>('parameters');
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
showTooltip: true,
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
},
{
title: 'apiTestDebug.desc',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: { x: '100%' },
columns,
});
propsRes.value.data = [
{
id: new Date().getTime(),
required: false,
name: 'asdasd',
type: 'string',
value: '',
desc: '',
},
{
id: new Date().getTime(),
required: true,
name: '23d23d',
type: 'string',
value: '',
desc: '',
},
] as any;
const sqlSourceColumns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
isNormal: true,
},
{
title: '',
slotName: 'operation',
width: 50,
},
];
const quoteSqlSourceDrawerVisible = ref(false);
function handleQuoteSqlSourceApply(sqlSource: any) {
condition.value.sqlSource = sqlSource;
emit('change');
}
function handleSqlSourceParamTableChange(resultArr: any[], isInit?: boolean) {
condition.value.sqlSource.params = [...resultArr];
if (!isInit) {
emit('change');
}
}
const extractParamsColumns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
width: 150,
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
typeOptions: [
{
label: t('apiTestDebug.globalParameter'),
value: 'global',
},
{
label: t('apiTestDebug.envParameter'),
value: 'env',
},
{
label: t('apiTestDebug.tempParameter'),
value: 'temp',
},
],
width: 110,
},
{
title: 'apiTestDebug.mode',
dataIndex: 'expressionType',
slotName: 'expressionType',
typeOptions: [
{
label: t('apiTestDebug.regular'),
value: 'regular',
},
{
label: 'JSONPath',
value: 'JSONPath',
},
{
label: 'XPath',
value: 'XPath',
},
],
width: 120,
},
{
title: 'apiTestDebug.range',
dataIndex: 'range',
slotName: 'range',
typeOptions: [
{
label: 'Body',
value: 'body',
},
{
label: 'Body (unescaped)',
value: 'body_unescaped',
},
{
label: 'Body as a Document',
value: 'body_document',
},
{
label: 'URL',
value: 'url',
},
{
label: 'Request Headers',
value: 'request_headers',
},
{
label: 'Response Headers',
value: 'response_headers',
},
{
label: 'Response Code',
value: 'response_code',
},
{
label: 'Response Message',
value: 'response_message',
},
],
width: 190,
},
{
title: 'apiTestDebug.expression',
dataIndex: 'expression',
slotName: 'expression',
width: 200,
},
{
title: '',
slotName: 'operation',
fixed: 'right',
moreAction: [
{
eventTag: 'copy',
label: 'common.copy',
},
{
eventTag: 'setting',
label: 'common.setting',
},
],
width: 80,
},
];
const disabledExpressionSuffix = ref(false);
function handleExtractParamTableChange(resultArr: any[], isInit?: boolean) {
condition.value.extractParams = [...resultArr];
if (!isInit) {
emit('change');
}
}
const extractParamsTableRef = ref<InstanceType<typeof paramTable>>();
const defaultExtractParamItem: Record<string, any> = {
name: '',
type: 'temp',
range: 'body',
expression: '',
expressionType: 'regular',
regexpMatchRule: 'expression',
resultMatchRule: 'random',
specifyMatchNum: 1,
xmlMatchContentType: 'xml',
moreSettingPopoverVisible: false,
};
const fastExtractionVisible = ref(false);
const activeRecord = ref<any>({ ...defaultExtractParamItem }); //
function showFastExtraction(record: Record<string, any>) {
activeRecord.value = { ...record };
fastExtractionVisible.value = true;
}
function handleExpressionChange(val: string) {
extractParamsTableRef.value?.addTableLine(val, 'expression');
}
/**
* 处理提取参数表格更多操作
*/
function handleExtractParamMoreActionSelect(event: ActionsItem, record: Record<string, any>) {
activeRecord.value = { ...record };
if (event.eventTag === 'copy') {
emit('copy');
} else if (event.eventTag === 'setting') {
record.moreSettingPopoverVisible = true;
}
}
/**
* 提取参数表格-应用更多设置
*/
function applyMoreSetting(record: Record<string, any>) {
condition.value.extractParams = condition.value.extractParams.map((e) => {
if (e.id === activeRecord.value.id) {
record.moreSettingPopoverVisible = false;
return {
...activeRecord.value,
moreSettingPopoverVisible: false,
};
}
return e;
});
emit('change');
}
/**
* 提取参数表格-保存快速提取的配置
*/
function handleFastExtractionApply(config: ExpressionConfig) {
condition.value.extractParams = condition.value.extractParams.map((e) => {
if (e.id === activeRecord.value.id) {
return {
...e,
...config,
};
}
return e;
});
fastExtractionVisible.value = false;
nextTick(() => {
extractParamsTableRef.value?.addTableLine();
});
emit('change');
}
</script>
<style lang="less" scoped>
.condition-content {
@apply flex-1 overflow-y-auto;
.ms-scroll-bar();
padding: 16px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
.script-name-container {
@apply flex items-center;
margin-right: 16px;
&:hover {
.edit-script-name-icon {
@apply visible;
}
}
.edit-script-name-icon {
@apply invisible cursor-pointer;
color: rgb(var(--primary-5));
}
}
}
.param-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<a-dropdown @select="addPrecondition">
<a-button type="outline">
<template #icon>
<icon-plus :size="14" />
</template>
{{ t(props.addText) }}
</a-button>
<template #content>
<a-doption v-for="key of props.conditionTypes" :key="key" :value="key">
{{ t(conditionTypeNameMap[key]) }}
</a-doption>
</template>
</a-dropdown>
<div v-if="$slots.titleRight" class="flex items-center">
<slot name="titleRight"></slot>
</div>
</div>
<div v-show="data.length > 0" class="flex h-[calc(100%-110px)] gap-[8px]">
<div class="h-full w-[20%] min-w-[220px]">
<conditionList
v-model:list="data"
:active-id="activeItem.id"
@active-change="handleListActiveChange"
@change="emit('change')"
/>
</div>
<conditionContent
v-model:data="activeItem"
:response="props.response"
:height-used="props.heightUsed"
@copy="copyListItem"
@delete="deleteListItem"
@change="emit('change')"
/>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import conditionContent from './content.vue';
import conditionList from './list.vue';
import { conditionTypeNameMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import { ConditionType } from '@/models/apiTest/debug';
const props = defineProps<{
list: Array<Record<string, any>>;
conditionTypes: Array<ConditionType>;
addText: string;
heightUsed?: number;
response?: string; //
}>();
const emit = defineEmits<{
(e: 'update:list', list: Array<Record<string, any>>): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
const data = useVModel(props, 'list', emit);
const activeItem = ref<Record<string, any>>({});
function handleListActiveChange(item: Record<string, any>) {
activeItem.value = item;
}
/**
* 复制列表项
*/
function copyListItem() {
const copyItem = {
...activeItem.value,
id: new Date().getTime(),
};
data.value.push(copyItem);
activeItem.value = copyItem;
emit('change');
}
/**
* 删除列表项
*/
function deleteListItem(id: string | number) {
data.value = data.value.filter((precondition) => precondition.id !== activeItem.value.id);
if (activeItem.value.id === id) {
[activeItem.value] = data.value;
}
emit('change');
}
const scriptEx = ref(`2023-12-04 11:19:28 INFO 9026fd6a 1-1 Thread started: 9026fd6a 1-1
2023-12-04 11:19:28 ERROR 9026fd6a 1-1 Problem in JSR223 script JSR223Sampler, message: {}
In file: inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' Encountered "import" at line 2, column 1.
in inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException
org.apache.http.client.method . . . '' at line number 2
`);
/**
* 添加前置条件
* @param value script | sql | waitTime
*/
function addPrecondition(value: string | number | Record<string, any> | undefined) {
const id = new Date().getTime();
switch (value) {
case 'script':
data.value.push({
id,
type: 'script',
name: t('apiTestDebug.preconditionScriptName'),
scriptType: 'manual',
enable: true,
script: '',
quoteScript: {
name: '',
script: scriptEx,
},
});
break;
case 'sql':
data.value.push({
id,
type: 'sql',
desc: '',
enable: true,
sqlSource: {
name: '',
script: scriptEx,
storageType: 'column',
params: [],
},
});
break;
case 'waitTime':
data.value.push({
id,
type: 'waitTime',
enable: true,
time: 1000,
});
break;
case 'extract':
data.value.push({
id,
type: 'extract',
enable: true,
extractParams: [],
});
break;
default:
break;
}
activeItem.value = data.value[data.value.length - 1];
emit('change');
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,129 @@
<template>
<MsList
v-model:active-item-key="activeItem.id"
v-model:focus-item-key="focusItemKey"
v-model:data="data"
mode="static"
item-key-field="id"
:item-border="false"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
item-class="mb-[4px] bg-white !p-[4px_8px]"
:item-more-actions="itemMoreActions"
active-item-class="!bg-[rgb(var(--primary-1))] text-[rgb(var(--primary-5))]"
:virtual-list-props="{ threshold: 100, height: '100%', fixedSize: true }"
draggable
@item-click="handleItemClick"
@more-action-select="handleMoreActionSelect"
@more-actions-close="focusItemKey = ''"
>
<template #title="{ item, index }">
<div class="flex items-center gap-[4px]">
<div
:class="`flex h-[16px] w-[16px] items-center justify-center rounded-full ${
activeItem.id === item.id ? ' bg-white' : 'bg-[var(--color-text-n8)]'
}`"
>
{{ index + 1 }}
</div>
<div>{{ t(conditionTypeNameMap[item.type]) }}</div>
</div>
</template>
<template #itemRight="{ item }">
<a-switch v-model:model-value="item.enable" size="small" type="line" @change="() => emit('change')" />
</template>
</MsList>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import MsList from '@/components/pure/ms-list/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { conditionTypeNameMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
list: Array<Record<string, any>>;
activeId?: string | number;
}>();
const emit = defineEmits<{
(e: 'update:list', list: Array<Record<string, any>>): void;
(e: 'activeChange', item: Record<string, any>): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
const data = useVModel(props, 'list', emit);
//
const focusItemKey = ref<any>('');
//
const activeItem = ref<Record<string, any>>({});
const itemMoreActions: ActionsItem[] = [
{
label: 'common.copy',
eventTag: 'copy',
},
{
label: 'common.delete',
eventTag: 'delete',
},
];
watch(
() => props.activeId,
(activeId) => {
activeItem.value = data.value.find((item) => item.id === activeId) || data.value[0] || {};
emit('activeChange', activeItem.value);
},
{
immediate: true,
}
);
function handleItemClick(item: Record<string, any>) {
activeItem.value = item;
emit('activeChange', item);
}
/**
* 复制列表项
* @param item 列表项
*/
function copyListItem(item: Record<string, any>) {
const copyItem = {
...item,
id: new Date().getTime(),
};
data.value.push(copyItem);
activeItem.value = copyItem;
emit('activeChange', activeItem.value);
}
/**
* 删除列表项
* @param item 列表项
*/
function deleteListItem(item: Record<string, any>) {
data.value = data.value.filter((precondition) => precondition.id !== item.id);
if (activeItem.value.id === item.id) {
[activeItem.value] = data.value;
}
emit('activeChange', activeItem.value);
}
/**
* 列表项-选择更多操作项
* @param event
* @param item
*/
function handleMoreActionSelect(event: ActionsItem, item: Record<string, any>) {
if (event.eventTag === 'copy') {
copyListItem(item);
} else if (event.eventTag === 'delete') {
deleteListItem(item);
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,275 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:width="680"
:title="t('apiTestDebug.fastExtraction')"
disabled-width-drag
@confirm="emit('apply', expressionForm)"
>
<div v-if="expressionForm.expressionType === 'regular'" class="h-[400px]">
<MsCodeEditor
:model-value="props.response"
theme="vs"
height="336px"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:language="LanguageEnum.JSON"
:show-full-screen="false"
show-language-change
read-only
/>
</div>
<div v-else-if="expressionForm.expressionType === 'JSONPath'" class="code-container">
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @pick="handlePathPick" />
</div>
<div v-else-if="expressionForm.expressionType === 'XPath'" class="code-container">
<MsXPathPicker :xml-string="props.response || ''" class="bg-white" @pick="handlePathPick" />
</div>
<a-form ref="expressionFormRef" :model="expressionForm" layout="vertical" class="mt-[16px]">
<a-form-item
v-if="expressionForm.expressionType === 'regular'"
field="expression"
:label="t('apiTestDebug.regularExpression')"
:rules="[{ required: true, message: t('apiTestDebug.regularExpressionRequired') }]"
asterisk-position="end"
>
<div class="form-input-wrapper">
<a-input
v-model:model-value="expressionForm.expression"
:placeholder="t('apiTestDebug.regularExpressionPlaceholder', { ex: '/<title>(.*?)</title>/' })"
class="flex-1"
/>
<a-button type="outline" :disabled="expressionForm.expression.trim() === ''" @click="testExpression">
{{ t('apiTestDebug.test') }}
</a-button>
</div>
</a-form-item>
<a-form-item
v-else-if="expressionForm.expressionType === 'JSONPath'"
field="expression"
label="JSONPath"
:rules="[{ required: true, message: t('apiTestDebug.JSONPathRequired') }]"
asterisk-position="end"
>
<div class="form-input-wrapper">
<a-input
v-model:model-value="expressionForm.expression"
:placeholder="t('apiTestDebug.JSONPathPlaceholder')"
class="flex-1"
/>
<a-button type="outline" :disabled="expressionForm.expression.trim() === ''" @click="testExpression">
{{ t('apiTestDebug.test') }}
</a-button>
</div>
</a-form-item>
<a-form-item
v-else
field="expression"
label="XPath"
:rules="[{ required: true, message: t('apiTestDebug.XPathRequired') }]"
asterisk-position="end"
>
<div class="form-input-wrapper">
<a-input
v-model:model-value="expressionForm.expression"
:placeholder="t('apiTestDebug.XPathPlaceholder')"
class="flex-1"
/>
<a-button type="outline" :disabled="expressionForm.expression.trim() === ''" @click="testExpression">
{{ t('apiTestDebug.test') }}
</a-button>
</div>
</a-form-item>
</a-form>
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="text-[var(--color-text-1)]">{{ t('apiTestDebug.matchResult') }}</div>
<a-tooltip :content-style="{ maxWidth: '500px' }">
<icon-question-circle
class="ml-[4px] cursor-pointer text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div
>{{ t('apiTestDebug.matchExpressionTip', { prefix: `${t('apiTestDebug.matchExpression')}: ` }) }}
</div>
<div>{{ t('apiTestDebug.matchGroupTip', { prefix: `${t('apiTestDebug.matchGroup')}: ` }) }}</div>
</template>
</a-tooltip>
</div>
<a-radio-group
v-if="expressionForm.expressionType === 'regular'"
v-model:model-value="expressionForm.regexpMatchRule"
type="button"
size="small"
>
<a-radio value="expression">{{ t('apiTestDebug.matchExpression') }}</a-radio>
<a-radio value="group">{{ t('apiTestDebug.matchGroup') }}</a-radio>
</a-radio-group>
</div>
<div class="match-result">
<div v-if="isMatched && matchResult.length === 0">{{ t('apiTestDebug.noMatchResult') }}</div>
<pre v-for="(e, i) of matchResult" :key="i">{{ e }}</pre>
</div>
</div>
<a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false" class="mt-[16px]">
<a-collapse-item :key="1">
<template #header>
<MsButton
type="text"
@click="() => (moreSettingActive.length > 0 ? (moreSettingActive = []) : (moreSettingActive = [1]))"
>
{{ t('apiTestDebug.moreSetting') }}
<icon-down v-if="moreSettingActive.length > 0" class="text-rgb(var(--primary-5))" />
<icon-right v-else class="text-rgb(var(--primary-5))" />
</MsButton>
</template>
<div class="mt-[16px]">
<moreSetting v-model:config="expressionForm" />
</div>
</a-collapse-item>
</a-collapse>
</MsDrawer>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import FormInstance from '@arco-design/web-vue';
import { JSONPath } from 'jsonpath-plus';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsJsonPathPicker from '@/components/pure/ms-jsonpath-picker/index.vue';
import MsXPathPicker from '@/components/pure/ms-jsonpath-picker/xpath.vue';
import moreSetting from './moreSetting.vue';
import { useI18n } from '@/hooks/useI18n';
import { matchXMLWithXPath } from '@/utils/xpath';
import { ExpressionConfig } from '@/models/apiTest/debug';
const props = defineProps<{
visible: boolean;
config: ExpressionConfig;
response?: string; //
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'apply', config: ExpressionConfig): void;
}>();
const { t } = useI18n();
const innerVisible = useVModel(props, 'visible', emit);
const expressionForm = ref({ ...props.config });
const expressionFormRef = ref<typeof FormInstance>();
const matchResult = ref<any[]>([]); //
const isMatched = ref(false); //
watch(
() => props.visible,
(val) => {
if (val) {
expressionForm.value = { ...props.config };
matchResult.value = [];
isMatched.value = false;
}
}
);
function handlePathPick(xpath: string) {
expressionForm.value.expression = xpath;
}
/*
* 测试表达式
*/
function testExpression() {
switch (props.config.expressionType) {
case 'XPath':
const nodes = matchXMLWithXPath(props.response || '', expressionForm.value.expression);
if (nodes) {
//
if (typeof nodes === 'boolean' || typeof nodes === 'string' || typeof nodes === 'number') {
matchResult.value = [nodes];
} else if (Array.isArray(nodes)) {
//
matchResult.value = nodes
.map((node) => node.textContent?.split('\n') || false)
.flat(Infinity)
.filter(Boolean);
} else {
//
matchResult.value = nodes.textContent ? [nodes.textContent] : [];
}
} else {
matchResult.value = [];
}
break;
case 'JSONPath':
try {
matchResult.value = JSONPath({
json: props.response ? JSON.parse(props.response) : '',
path: expressionForm.value.expression,
});
} catch (error) {
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression });
}
break;
case 'regular':
default:
// /g
const matchesIterator = props.response?.matchAll(
new RegExp(expressionForm.value.expression.replace(/^\/|\/$|\/g$/g, ''), 'g')
);
if (matchesIterator) {
const matches = Array.from(matchesIterator);
try {
if (expressionForm.value.regexpMatchRule === 'expression') {
//
matchResult.value = matches.map((e) => e[0]) || [];
} else {
matchResult.value = matches.map((e) => e.slice(1)).flat(Infinity) || []; //
}
} catch (error) {
//
matchResult.value = [];
isMatched.value = true;
}
} else {
matchResult.value = [];
}
break;
}
isMatched.value = true;
}
const moreSettingActive = ref<number[]>([]);
</script>
<style lang="less" scoped>
.form-input-wrapper {
@apply flex w-full items-center justify-between;
gap: 12px;
}
.code-container {
padding: 12px;
height: 400px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
}
.match-result {
@apply overflow-y-auto bg-white;
.ms-scroll-bar();
margin-top: 12px;
padding: 12px;
min-height: 32px;
max-height: 300px;
border-radius: var(--border-radius-small);
color: rgb(var(--primary-5));
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<div>
<div v-if="expressionForm.expressionType === 'regular' && props.isPopover" class="mb-[16px]">
<div class="mb-[8px] text-[14px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.expressionMatchRule') }}
</div>
<a-radio-group v-model:model-value="expressionForm.regexpMatchRule" size="small">
<a-radio value="expression">
<div class="flex items-center">
{{ t('apiTestDebug.matchExpression') }}
<a-tooltip :content="t('apiTestDebug.matchExpressionTip')" :content-style="{ maxWidth: '500px' }">
<icon-question-circle
class="ml-[4px] cursor-pointer text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-radio>
<a-radio value="group">
<div class="flex items-center">
{{ t('apiTestDebug.matchGroup') }}
<a-tooltip :content="t('apiTestDebug.matchGroupTip')" :content-style="{ maxWidth: '500px' }">
<icon-question-circle
class="ml-[4px] cursor-pointer text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-radio>
</a-radio-group>
</div>
<div class="mb-[16px]">
<div class="mb-[8px] text-[14px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.resultMatchRule') }}
</div>
<a-radio-group v-model:model-value="expressionForm.resultMatchRule" size="small">
<a-radio value="random">
<div class="flex items-center">
{{ t('apiTestDebug.randomMatch') }}
<a-tooltip :content="t('apiTestDebug.randomMatchTip')" :content-style="{ maxWidth: '400px' }">
<icon-question-circle
class="ml-[4px] cursor-pointer text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-radio>
<a-radio value="specify">
<div class="flex items-center">
{{ t('apiTestDebug.specifyMatch') }}
<a-tooltip :content="t('apiTestDebug.specifyMatchTip')" :content-style="{ maxWidth: '400px' }">
<icon-question-circle
class="ml-[4px] cursor-pointer text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-radio>
<a-radio value="all">
<div class="flex items-center">
{{ t('apiTestDebug.allMatch') }}
<a-tooltip :content="t('apiTestDebug.allMatchTip')" :content-style="{ maxWidth: '400px' }">
<icon-question-circle
class="ml-[4px] cursor-pointer text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-radio>
</a-radio-group>
</div>
<div v-if="expressionForm.resultMatchRule === 'specify'" class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.specifyMatchResult') }}
</div>
<div class="flex items-center gap-[8px]">
{{ t('apiTestDebug.index') }}
<a-input-number v-model:model-value="expressionForm.specifyMatchNum" :min="1" class="w-[80px]" />
{{ t('apiTestDebug.unit') }}
</div>
</div>
<div v-if="expressionForm.expressionType === 'XPath'" class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.contentType') }}
</div>
<a-radio-group v-model:model-value="expressionForm.xmlMatchContentType" size="small">
<a-radio value="xml"> XML </a-radio>
<a-radio value="html"> HTML </a-radio>
</a-radio-group>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
import { ExpressionConfig } from '@/models/apiTest/debug';
const props = defineProps<{
config: ExpressionConfig;
isPopover?: boolean; //
}>();
const emit = defineEmits<{
(e: 'update:config', config: ExpressionConfig): void;
}>();
const { t } = useI18n();
const expressionForm = useVModel(props, 'config', emit);
</script>
<style lang="less" scoped></style>

View File

@ -1,5 +1,5 @@
<template>
<MsBaseTable v-bind="propsRes" id="headerTable" :hoverable="false" no-disable v-on="propsEvent">
<MsBaseTable v-bind="propsRes" :hoverable="false" no-disable v-on="propsEvent">
<!-- 表格头 slot -->
<template #encodeTitle>
<div class="flex items-center text-[var(--color-text-3)]">
@ -42,7 +42,7 @@
v-model:model-value="record[columnConfig.dataIndex as string]"
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
class="param-input"
@input="(val) => addTableLine(val)"
@input="(val) => addTableLine(val, 'name')"
/>
</a-popover>
</template>
@ -69,6 +69,25 @@
@change="(val) => handleTypeChange(val, record)"
/>
</template>
<template #expressionType="{ record, columnConfig }">
<a-select
v-model:model-value="record.expressionType"
:options="columnConfig.typeOptions || []"
class="param-input w-[110px]"
@change="(val) => handleExpressionTypeChange(val)"
/>
</template>
<template #range="{ record, columnConfig }">
<a-select
v-model:model-value="record.range"
:options="columnConfig.typeOptions || []"
class="param-input w-[180px]"
@change="(val) => handleRangeChange(val)"
/>
</template>
<template #expression="{ record, rowIndex, columnConfig }">
<slot name="expression" :record="record" :row-index="rowIndex" :column-config="columnConfig"></slot>
</template>
<template #value="{ record, columnConfig }">
<a-popover
v-if="columnConfig.isNormal"
@ -88,13 +107,13 @@
v-model:model-value="record.value"
class="param-input"
:placeholder="t('apiTestDebug.commonPlaceholder')"
@input="(val) => addTableLine(val)"
@input="(val) => addTableLine(val, 'value')"
/>
</a-popover>
<MsParamsInput
v-else
v-model:value="record.value"
@change="addTableLine"
@change="(val) => addTableLine(val, 'value')"
@dblclick="quickInputParams(record)"
@apply="handleParamSettingApply"
/>
@ -104,16 +123,16 @@
<a-input-number
v-model:model-value="record.min"
:placeholder="t('apiTestDebug.paramMin')"
class="param-input"
@input="(val) => addTableLine(val)"
></a-input-number>
class="param-input param-input-number"
@input="(val) => addTableLine(val || '', 'min')"
/>
<div class="mx-[4px]"></div>
<a-input-number
v-model:model-value="record.max"
:placeholder="t('apiTestDebug.paramMax')"
class="param-input"
@input="(val) => addTableLine(val)"
></a-input-number>
@input="(val) => addTableLine(val || '', 'max')"
/>
</div>
</template>
<template #tag="{ record, columnConfig }">
@ -134,14 +153,14 @@
v-model:model-value="record[columnConfig.dataIndex as string]"
:max-tag-count="1"
class="param-input"
@change="addTableLine"
@change="(val) => addTableLine(val, 'tag')"
/>
</a-popover>
</template>
<template #desc="{ record, columnConfig }">
<paramDescInput
v-model:desc="record[columnConfig.dataIndex as string]"
@input="addTableLine"
@input="(val) => addTableLine(val, 'desc')"
@dblclick="quickInputDesc(record)"
@change="handleDescChange"
/>
@ -152,13 +171,19 @@
size="small"
class="param-input-switch"
type="line"
@change="(val) => addTableLine(val.toString())"
@change="(val) => addTableLine(val.toString(), 'encode')"
/>
</template>
<template #mustContain="{ record, columnConfig }">
<a-checkbox v-model:model-value="record[columnConfig.dataIndex as string]" @change="(val) => addTableLine(val)" />
</template>
<template #operation="{ record, rowIndex, columnConfig }">
<slot name="operationPre" :record="record" :row-index="rowIndex" :column-config="columnConfig"></slot>
<MsTableMoreAction
v-if="columnConfig.moreAction"
:list="getMoreActionList(columnConfig.moreAction, record)"
@select="(e) => handleMoreActionSelect(e, record)"
/>
<a-trigger
v-if="columnConfig.format && columnConfig.format !== RequestBodyFormat.X_WWW_FORM_URLENCODED"
trigger="click"
@ -172,7 +197,7 @@
v-model:model-value="record.contentType"
:options="Object.values(RequestContentTypeEnum).map((e) => ({ label: e, value: e }))"
allow-create
@change="(val) => addTableLine(val as string)"
@change="(val) => addTableLine(val as string, 'contentType')"
/>
</div>
</template>
@ -182,7 +207,7 @@
v-model:model-value="record.enable"
size="small"
type="line"
@change="(val) => addTableLine(val)"
@change="(val) => addTableLine(val, 'enable')"
/>
<icon-minus-circle
v-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
@ -261,6 +286,8 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumnData } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsParamsInput from '@/components/business/ms-params-input/index.vue';
@ -295,6 +322,7 @@
typeOptions?: { label: string; value: string }[]; // type
typeTitleTooltip?: string; // type tooltip
hasEnable?: boolean; // operation enable
moreAction?: ActionsItem[]; // operation
format?: RequestBodyFormat | 'query' | 'rest'; // operation
};
@ -317,6 +345,7 @@
disabled?: boolean; //
showSelectorAll?: boolean; //
isSimpleSetting?: boolean; // Column
response?: string; //
}>(),
{
selectable: true,
@ -342,14 +371,18 @@
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change', data: any[], isInit?: boolean): void;
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
}>();
const { t } = useI18n();
const tableStore = useTableStore();
async function initColumns() {
if (props.showSetting && props.tableKey) {
await tableStore.initColumn(props.tableKey, props.columns);
}
}
initColumns();
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
tableKey: props.showSetting ? props.tableKey : undefined,
@ -399,14 +432,21 @@
/**
* 当表格输入框变化时给参数表格添加一行数据行
* @param val 输入值
* @param key 当前列的 key
* @param isForce 是否强制添加
*/
function addTableLine(val?: string | number | boolean | (string | number | boolean)[], isForce?: boolean) {
function addTableLine(
val?: string | number | boolean | (string | number | boolean)[],
key?: string,
isForce?: boolean
) {
const lastData = propsRes.value.data[propsRes.value.data.length - 1];
const isNotChange = Object.keys(props.defaultParamItem).every(
(key) => JSON.stringify(lastData[key]) === JSON.stringify(props.defaultParamItem[key])
);
if (isForce || (val !== '' && val !== undefined && !isNotChange)) {
// key key
const isNotChange =
val === undefined || key === undefined
? Object.keys(props.defaultParamItem).every((e) => lastData[e] === props.defaultParamItem[e])
: JSON.stringify(lastData[key]) === JSON.stringify(props.defaultParamItem[key]);
if (isForce || (val !== '' && !isNotChange)) {
propsRes.value.data.push({
id: new Date().getTime(),
...props.defaultParamItem,
@ -434,12 +474,12 @@
activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false;
clearQuickInputParam();
addTableLine(quickInputParamValue.value, true);
addTableLine(quickInputParamValue.value, 'value', true);
emit('change', propsRes.value.data);
}
function handleParamSettingApply(val: string | number) {
addTableLine(val);
addTableLine(val, 'value');
}
const showQuickInputDesc = ref(false);
@ -460,7 +500,7 @@
activeQuickInputRecord.value.desc = quickInputDescValue.value;
showQuickInputDesc.value = false;
clearQuickInputDesc();
addTableLine(quickInputDescValue.value, true);
addTableLine(quickInputDescValue.value, 'desc', true);
emit('change', propsRes.value.data);
}
@ -472,7 +512,7 @@
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[],
record: Partial<Param>
) {
addTableLine(val as string);
addTableLine(val as string, 'type');
// Content-Type
if (record.contentType) {
if (val === 'file') {
@ -484,6 +524,42 @@
}
}
}
function handleExpressionTypeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
addTableLine(val as string, 'expressionType');
}
function handleRangeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
addTableLine(val as string, 'range');
}
/**
* 获取更多操作按钮列表
* @param actions 按钮列表
* @param record 当前行数据
*/
function getMoreActionList(actions: ActionsItem[], record: Record<string, any>) {
if (props.columns.findIndex((e) => e.dataIndex === 'expression') !== -1) {
// expressionexpression
if (record.expression === '' || record.expression === undefined || record.expression === null) {
return actions.map((e) => ({ ...e, disabled: true }));
}
return actions;
}
return actions;
}
function handleMoreActionSelect(event: ActionsItem, record: Record<string, any>) {
emit('moreActionSelect', event, record);
}
defineExpose({
addTableLine,
});
</script>
<style lang="less" scoped>
@ -491,10 +567,12 @@
background-color: var(--color-text-n9);
}
:deep(.arco-table-cell-align-left) {
padding: 16px 4px;
padding: 16px 12px;
}
:deep(.arco-table-td) {
.arco-table-cell {
padding: 12px 2px;
}
:deep(.arco-table-cell) {
padding: 11px 4px;
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
@ -510,6 +588,24 @@
}
}
}
:deep(.param-input-number) {
@apply pr-0;
.arco-input {
@apply text-right;
}
.arco-input-suffix {
@apply hidden;
}
&:hover,
&.arco-input-focus {
.arco-input {
@apply text-left;
}
.arco-input-suffix {
@apply inline-flex;
}
}
}
.content-type-trigger-content {
@apply bg-white;
@ -517,20 +613,6 @@
border-radius: var(--border-radius-small);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
}
.param-input {
.param-input-mock-icon {
@apply invisible;
}
&:hover,
&.arco-input-focus {
.param-input-mock-icon {
@apply visible cursor-pointer;
&:hover {
color: rgb(var(--primary-5));
}
}
}
}
.param-popover-title {
@apply font-medium;

View File

@ -0,0 +1,155 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:width="960"
:title="t('apiTestDebug.quoteSource')"
:ok-disabled="selectedKey === ''"
@confirm="handleConfirm"
>
<div class="mb-[16px] flex items-center justify-between">
<div class="text-[var(--color-text-1)]">{{ t('apiTestDebug.sourceList') }}</div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('project.projectVersion.searchPlaceholder')"
class="w-[230px]"
allow-clear
@search="searchSource"
@press-enter="searchSource"
/>
</div>
<MsBaseTable v-bind="propsRes" v-model:selected-key="selectedKey" v-on="propsEvent">
<template #timeout="{ record }">
<a-tooltip :content="record.timeout?.toLocaleString()">
<div class="one-line-text">{{ record.timeout?.toLocaleString() }}</div>
</a-tooltip>
</template>
</MsBaseTable>
</MsDrawer>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
visible: boolean;
selectedKey?: string;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'apply', value: any): void;
}>();
const { t } = useI18n();
const innerVisible = useVModel(props, 'visible', emit);
const keyword = ref('');
const selectedKey = ref(props.selectedKey || '');
const columns: MsTableColumn = [
{
title: 'apiTestDebug.sqlSourceName',
dataIndex: 'name',
showTooltip: true,
},
{
title: 'apiTestDebug.driver',
dataIndex: 'driver',
showTooltip: true,
},
{
title: 'apiTestDebug.username',
dataIndex: 'username',
showTooltip: true,
width: 150,
},
{
title: 'apiTestDebug.maxConnection',
dataIndex: 'maxConnection',
width: 110,
},
{
title: 'apiTestDebug.timeout',
dataIndex: 'timeout',
slotName: 'timeout',
align: 'right',
width: 120,
},
];
async function loadSource() {
return Promise.resolve({
list: [
{
id: '1',
name: 'test',
driver: 'com.mysql.cj.jdbc.Driver',
username: 'root',
maxConnection: 10,
timeout: 1000,
storageType: 'column',
params: [],
script: 'select * from test1',
},
{
id: '2',
name: 'test2',
driver: 'com.mysql.cj.jdbc.Driver',
username: 'root',
maxConnection: 10,
timeout: 1000,
storageType: 'column',
params: [],
script: 'select * from test2',
},
{
id: '3',
name: 'test3',
driver: 'com.mysql.cj.jdbc.Driver',
username: 'root',
maxConnection: 10,
timeout: 10000000000,
storageType: 'result',
params: [],
script: 'select * from test3',
},
],
total: 99,
});
}
const { propsRes, propsEvent, setLoadListParams, loadList } = useTable(loadSource, {
columns,
scroll: { x: '100%' },
heightUsed: 300,
selectable: true,
showSelectorAll: false,
selectorType: 'radio',
firstColumnWidth: 44,
});
function searchSource() {
setLoadListParams({
keyword: keyword.value,
});
loadList();
}
searchSource();
function handleConfirm() {
innerVisible.value = false;
emit(
'apply',
propsRes.value.data.find((item) => item.id === selectedKey.value)
);
}
</script>
<style lang="less" scoped>
:deep(.arco-table-cell-align-left) {
padding-left: 9px;
}
</style>

View File

@ -4,12 +4,24 @@
</a-button>
<MsDrawer
v-model:visible="showBatchAddParamDrawer"
:title="t('common.batchAdd')"
:width="680"
:ok-text="t('apiTestDebug.apply')"
disabled-width-drag
@confirm="applyBatchParams"
>
<template #title>
{{ t('common.batchAdd') }}
<a-tooltip position="right">
<icon-exclamation-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div>{{ t('apiTestDebug.batchAddParamsTip2') }} </div>
<div>{{ t('apiTestDebug.batchAddParamsTip3') }} </div>
</template>
</a-tooltip>
</template>
<div class="flex h-full">
<MsCodeEditor
v-if="showBatchAddParamDrawer"
@ -20,14 +32,9 @@
:show-full-screen="false"
>
<template #title>
<div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('apiTestDebug.batchAddParamsTip') }}
</div>
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('apiTestDebug.batchAddParamsTip2') }}
</div>
</div>
</template>
</MsCodeEditor>
</div>
@ -74,13 +81,13 @@
*/
function applyBatchParams() {
const arr = batchParamsCode.value.replaceAll('\r', '\n').split('\n'); //
const resultArr = arr
.map((item, i) => {
const [name, value] = item.split(':');
if (name || value) {
return {
const tempObj: Record<string, any> = {}; //
for (let i = 0; i < arr.length; i++) {
const [name, value] = arr[i].split(':');
if (name) {
tempObj[name.trim()] = {
id: new Date().getTime() + i,
name: name?.trim(),
name: name.trim(),
value: value?.trim(),
required: false,
type: 'string',
@ -91,12 +98,10 @@
encode: false,
};
}
return null;
})
.filter((item) => item);
}
showBatchAddParamDrawer.value = false;
batchParamsCode.value = '';
emit('apply', resultArr);
emit('apply', Object.values(tempObj));
}
</script>

View File

@ -73,6 +73,7 @@
import { useVModel } from '@vueuse/core';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
import batchAddKeyVal from './batchAddKeyVal.vue';
@ -154,6 +155,7 @@
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
align: 'center',
width: 200,
},
{
@ -244,12 +246,12 @@
//
const currentCodeLanguage = computed(() => {
if (format.value === RequestBodyFormat.JSON) {
return 'json';
return LanguageEnum.JSON;
}
if (format.value === RequestBodyFormat.XML) {
return 'xml';
return LanguageEnum.XML;
}
return 'plaintext';
return LanguageEnum.PLAINTEXT;
});
function formatChange() {

View File

@ -8,6 +8,7 @@
:columns="columns"
:height-used="heightUsed"
:scroll="scroll"
draggable
@change="handleParamTableChange"
/>
</template>
@ -57,31 +58,14 @@
},
];
const heightUsed = ref<number | undefined>(undefined);
const heightUsed = computed(() => {
if (props.layout === 'horizontal') {
return 422;
}
return 422 + props.secondBoxHeight;
});
const scroll = computed(() => (props.layout === 'horizontal' ? { x: '700px' } : { x: '100%' }));
watch(
() => props.layout,
(val) => {
heightUsed.value = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight;
},
{
immediate: true,
}
);
watch(
() => props.secondBoxHeight,
(val) => {
if (props.layout === 'vertical') {
heightUsed.value = 422 + val;
}
},
{
immediate: true,
}
);
/**
* 批量参数代码转换为参数表格数据
*/

View File

@ -66,6 +66,7 @@
:max="0.98"
min="10px"
:direction="activeLayout"
second-container-class="!overflow-y-hidden"
@expand-change="handleExpandChange"
>
<template #first>
@ -103,8 +104,14 @@
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="activeDebug.activeTab === RequestComposition.PREFIX"
v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION"
v-model:params="activeDebug.preconditions"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION"
v-model:params="activeDebug.postConditions"
:response="activeDebug.response.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
@ -166,6 +173,7 @@
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 debugRest from './rest.vue';
@ -191,8 +199,7 @@
binarySend: false,
raw: '',
};
const debugTabs = ref<TabItem[]>([
{
const defaultDebugParams = {
id: initDefaultId,
moduleProtocol: 'http',
activeTab: RequestComposition.HEADER,
@ -210,14 +217,87 @@
password: '',
},
preconditions: [],
postConditions: [],
setting: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
redirect: 'follow',
},
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
}
}
}`,
}, //
};
const debugTabs = ref<TabItem[]>([cloneDeep(defaultDebugParams)]);
const debugUrl = ref('');
const activeDebug = ref<TabItem>(debugTabs.value[0]);
@ -232,28 +312,8 @@
function addDebugTab() {
const id = `debug-${Date.now()}`;
debugTabs.value.push({
...cloneDeep(defaultDebugParams),
id,
moduleProtocol: 'http',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSave: false,
headerParams: [],
bodyParams: cloneDeep(defaultBodyParams),
queryParams: [],
restParams: [],
authParams: {
authType: 'none',
account: '',
password: '',
},
setting: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
redirect: 'follow',
},
});
activeTab.value = id;
}
@ -295,12 +355,12 @@
label: RequestComposition.REST,
},
{
value: RequestComposition.PREFIX,
value: RequestComposition.PRECONDITION,
label: t('apiTestDebug.prefix'),
},
{
value: RequestComposition.POST_CONDITION,
label: t('apiTestDebug.postCondition'),
label: t('apiTestDebug.post'),
},
{
value: RequestComposition.ASSERTION,
@ -331,9 +391,15 @@
watch(
() => splitBoxSize.value,
debounce((val) => {
// 300ms
if (splitContainerRef.value) {
if (typeof val === 'string' && val.includes('px')) {
val = Number(val.split('px')[0]);
secondBoxHeight.value = splitContainerRef.value.clientHeight - val;
} else {
secondBoxHeight.value = splitContainerRef.value.clientHeight * (1 - val);
}
}
}, 300),
{
immediate: true,

View File

@ -0,0 +1,53 @@
<template>
<condition
v-model:list="postConditions"
:condition-types="['script', 'sql', 'waitTime', 'extract']"
add-text="apiTestDebug.postCondition"
:response="props.response"
:height-used="heightUsed"
@change="emit('change')"
>
<template #titleRight>
<a-switch v-model:model-value="openGlobalPostCondition" size="small" type="line"></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPostCondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPostConditionTip')" position="left">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</template>
</condition>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import condition from '../../../components/condition/index.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
secondBoxHeight?: number;
layout: 'horizontal' | 'vertical';
response?: string; //
}>();
const emit = defineEmits<{
(e: 'update:params', params: any[]): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
//
const openGlobalPostCondition = ref(false);
const postConditions = useVModel(props, 'params', emit);
const heightUsed = computed(() => {
if (props.layout === 'horizontal') {
return 422;
}
return 422 + (props.secondBoxHeight || 0);
});
</script>
<style lang="less" scoped></style>

View File

@ -1,320 +1,32 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<a-dropdown @select="addPrecondition">
<a-button type="outline">
<template #icon>
<icon-plus :size="14" />
</template>
{{ t('apiTestDebug.precondition') }}
</a-button>
<template #content>
<a-doption value="script">{{ t('apiTestDebug.script') }}</a-doption>
<a-doption value="sql">{{ t('apiTestDebug.sql') }}</a-doption>
<a-doption value="waitTime">{{ t('apiTestDebug.waitTime') }}</a-doption>
</template>
</a-dropdown>
<div class="flex items-center">
<condition
v-model:list="preconditions"
:condition-types="['script', 'sql', 'waitTime']"
add-text="apiTestDebug.precondition"
@change="emit('change')"
>
<template #titleRight>
<a-switch v-model:model-value="openGlobalPrecondition" size="small" type="line"></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPrecondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPreconditionTip')" position="top">
<a-tooltip :content="t('apiTestDebug.openGlobalPreconditionTip')" position="left">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</div>
<div v-show="preconditions.length > 0" class="flex h-[calc(100%-110px)] gap-[8px]">
<div class="h-full w-[20%] min-w-[220px]">
<MsList
v-model:active-item-key="activeItem.id"
v-model:focus-item-key="focusItemKey"
v-model:data="preconditions"
mode="static"
item-key-field="id"
:item-border="false"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
item-class="mb-[4px] bg-white !p-[4px_8px]"
:item-more-actions="itemMoreActions"
active-item-class="!bg-[rgb(var(--primary-1))] text-[rgb(var(--primary-5))]"
:virtual-list-props="{ height: '100%', fixedSize: true }"
draggable
@item-click="handlePreconditionItemClick"
@more-action-select="handlePreconditionMoreActionSelect"
@more-actions-close="focusItemKey = ''"
>
<template #title="{ item, index }">
<div class="flex items-center gap-[4px]">
<div
:class="`flex h-[16px] w-[16px] items-center justify-center rounded-full ${
activeItem.id === item.id ? ' bg-white' : 'bg-[var(--color-text-n8)]'
}`"
>
{{ index + 1 }}
</div>
<div>{{ typeMap[item.type] }}</div>
</div>
</template>
<template #itemRight="{ item }">
<a-switch v-model:model-value="item.enable" size="small" type="line"></a-switch>
</template>
</MsList>
</div>
<div class="precondition-content">
<!-- 前置条件-脚本操作 -->
<template v-if="activeItem.type === 'script'">
<a-radio-group v-model:model-value="activeItem.scriptType" size="small" class="mb-[16px]">
<a-radio value="manual">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio value="quote">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<div
v-if="activeItem.scriptType === 'manual'"
class="relative rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
<a-input
ref="scriptNameInputRef"
v-model:model-value="activeItem.name"
:placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')"
:max-length="255"
show-word-limit
size="small"
@press-enter="isShowEditScriptNameInput = false"
@blur="isShowEditScriptNameInput = false"
/>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<a-tooltip :content="activeItem.name">
<div class="script-name-container">
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ activeItem.name }}
</div>
<MsIcon
type="icon-icon_edit_outlined"
class="edit-script-name-icon"
@click="showEditScriptNameInput"
/>
</div>
</a-tooltip>
<a-popover class="h-auto" position="top">
<div class="text-[rgb(var(--primary-5))]">{{ t('apiTestDebug.scriptEx') }}</div>
<template #content>
<div class="mb-[8px] flex items-center justify-between">
<div class="text-[14px] font-medium text-[var(--color-text-1)]">
{{ t('apiTestDebug.scriptEx') }}
</div>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScriptEx"
>
{{ t('common.copy') }}
</a-button>
</div>
<div class="flex h-[412px]">
<MsCodeEditor
v-model:model-value="scriptEx"
class="flex-1"
theme="MS-text"
width="500px"
height="388px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
</template>
</a-popover>
</div>
<div class="flex items-center gap-[8px]">
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini">
<template #icon>
<MsIcon type="icon-icon_undo_outlined" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.revoke') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="clearScript">
<template #icon>
<MsIcon type="icon-icon_clear" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.clear') }}
</a-button>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="() => copyPrecondition(activeItem)"
>
{{ t('common.copy') }}
</a-button>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="() => deletePrecondition(activeItem)"
>
{{ t('common.delete') }}
</a-button>
</div>
</div>
</div>
<div v-else class="flex h-[calc(100%-47px)] flex-col">
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ activeItem.quoteScript.name || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium">
{{ t('apiTestDebug.quote') }}
</MsButton>
</div>
<a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit">
<a-radio value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio>
<a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio>
</a-radio-group>
<MsBaseTable v-show="commonScriptShowType === 'parameters'" v-bind="propsRes" v-on="propsEvent">
<template #value="{ record }">
<a-tooltip :content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')">
<div
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
>
<div>*</div>
</div>
</a-tooltip>
{{ record.type }}
</template>
</MsBaseTable>
<div v-show="commonScriptShowType === 'scriptContent'" class="h-[calc(100%-76px)]">
<MsCodeEditor
v-model:model-value="activeItem.quoteScript.script"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
</div>
</template>
<!-- 前置条件-SQL操作 -->
<template v-else-if="activeItem.type === 'sql'">
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
<a-input
v-model:model-value="activeItem.desc"
:placeholder="t('apiTestDebug.commonPlaceholder')"
:max-length="255"
show-word-limit
/>
</div>
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ activeItem.sqlSource.name || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium">
{{ t('apiTestDebug.quoteSource') }}
</MsButton>
</div>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.sqlScript') }}</div>
<div class="mb-[16px] h-[400px]">
<MsCodeEditor
v-model:model-value="activeItem.sqlSource.script"
theme="MS-text"
height="376px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
<div class="mb-[16px]">
<div class="mb-[8px] flex items-center text-[var(--color-text-1)]">
{{ t('apiTestDebug.storageType') }}
<a-tooltip position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div>{{ t('apiTestDebug.storageTypeTip1') }}</div>
<div>{{ t('apiTestDebug.storageTypeTip2') }}</div>
</template>
</a-tooltip>
</div>
<a-radio-group
v-model:model-value="activeItem.sqlSource.storageType"
size="small"
type="button"
class="w-fit"
>
<a-radio value="column">{{ t('apiTestDebug.storageByCol') }}</a-radio>
<a-radio value="result">{{ t('apiTestDebug.storageByResult') }}</a-radio>
</a-radio-group>
</div>
<div v-if="activeItem.sqlSource.storageType === 'column'" class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByCol') }}</div>
<a-input
v-model:model-value="activeItem.sqlSource.storageByCol"
:placeholder="t('apiTestDebug.storageByColPlaceholder', { a: '{id_1}', b: '{username_1}' })"
/>
</div>
<div v-else class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div>
<a-input
v-model:model-value="activeItem.sqlSource.storageByResult"
:placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })"
/>
</div>
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.extractParameter') }}</div>
<paramTable
v-model:params="activeItem.sqlSource.params"
:columns="sqlSourceColumns"
:selectable="false"
@change="handleParamTableChange"
/>
</div>
</template>
<!-- 前置条件-等待时间 -->
<div v-else>
<div class="mb-[8px] flex items-center">
{{ t('apiTestDebug.waitTime') }}
<div class="text-[var(--color-text-4)]">(ms)</div>
</div>
<a-input-number v-model:model-value="activeItem.time" mode="button" :step="100" :min="0" class="w-[160px]" />
</div>
</div>
</div>
</condition>
</template>
<script setup lang="ts">
import { useClipboard, useVModel } from '@vueuse/core';
import { InputInstance, Message } from '@arco-design/web-vue';
import { useVModel } from '@vueuse/core';
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 MsList from '@/components/pure/ms-list/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
import condition from '../../../components/condition/index.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
const emit = defineEmits<{
(e: 'update:params', params: any[]): void;
@ -325,273 +37,6 @@
//
const openGlobalPrecondition = ref(false);
const preconditions = useVModel(props, 'params', emit);
//
const focusItemKey = ref<any>('');
//
const activeItem = ref(preconditions.value[0] || {});
const typeMap = {
script: t('apiTestDebug.script'),
sql: t('apiTestDebug.sql'),
waitTime: t('apiTestDebug.waitTime'),
};
const itemMoreActions: ActionsItem[] = [
{
label: 'common.copy',
eventTag: 'copy',
},
{
label: 'project.fileManagement.delete',
eventTag: 'delete',
},
];
const scriptEx = ref(`2023-12-04 11:19:28 INFO 9026fd6a 1-1 Thread started: 9026fd6a 1-1
2023-12-04 11:19:28 ERROR 9026fd6a 1-1 Problem in JSR223 script JSR223Sampler, message: {}
In file: inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' Encountered "import" at line 2, column 1.
in inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException
org.apache.http.client.method . . . '' at line number 2
`);
const { copy, isSupported } = useClipboard();
function copyScriptEx() {
if (isSupported) {
copy(scriptEx.value);
Message.success(t('apiTestDebug.scriptExCopySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
/**
* 添加前置条件
* @param value script | sql | waitTime
*/
function addPrecondition(value: string | number | Record<string, any> | undefined) {
const id = new Date().getTime();
switch (value) {
case 'script':
preconditions.value.push({
id,
type: 'script',
name: t('apiTestDebug.preconditionScriptName'),
scriptType: 'manual',
enable: true,
script: '',
quoteScript: {
name: '',
script: scriptEx,
},
});
break;
case 'sql':
preconditions.value.push({
id,
type: 'sql',
desc: '',
enable: true,
sqlSource: {
name: '',
script: scriptEx,
storageType: 'column',
params: [],
},
});
break;
case 'waitTime':
preconditions.value.push({
id,
type: 'waitTime',
enable: true,
time: 1000,
});
break;
default:
break;
}
activeItem.value = preconditions.value[preconditions.value.length - 1];
emit('change');
}
function handlePreconditionItemClick(item: any) {
activeItem.value = item;
}
/**
* 复制前置条件
* @param item 前置条件项
*/
function copyPrecondition(item: Record<string, any>) {
const copyItem = {
...item,
id: new Date().getTime(),
};
preconditions.value.push(copyItem);
activeItem.value = copyItem;
emit('change');
}
function deletePrecondition(item: Record<string, any>) {
preconditions.value = preconditions.value.filter((precondition) => precondition.id !== item.id);
if (activeItem.value.id === item.id) {
[activeItem.value] = preconditions.value;
}
emit('change');
}
/**
* 前置条件列表项-选择更多操作项
* @param event
* @param item
*/
function handlePreconditionMoreActionSelect(event: ActionsItem, item: Record<string, any>) {
if (event.eventTag === 'copy') {
copyPrecondition(item);
} else if (event.eventTag === 'delete') {
deletePrecondition(item);
}
}
function clearScript() {
activeItem.value.script = '';
}
//
const isShowEditScriptNameInput = ref(false);
const scriptNameInputRef = ref<InputInstance>();
function showEditScriptNameInput() {
isShowEditScriptNameInput.value = true;
nextTick(() => {
scriptNameInputRef.value?.focus();
});
}
const commonScriptShowType = ref<'parameters' | 'scriptContent'>('parameters');
const heightUsed = ref<number | undefined>(undefined);
const scroll = computed(() => (props.layout === 'horizontal' ? { x: '700px' } : { x: '100%' }));
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
showTooltip: true,
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
},
{
title: 'apiTestDebug.desc',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: scroll.value,
heightUsed: heightUsed.value,
columns,
});
propsRes.value.data = [
{
id: new Date().getTime(),
required: false,
name: 'asdasd',
type: 'string',
value: '',
desc: '',
},
{
id: new Date().getTime(),
required: true,
name: '23d23d',
type: 'string',
value: '',
desc: '',
},
] as any;
watch(
() => props.layout,
(val) => {
heightUsed.value = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight;
},
{
immediate: true,
}
);
watch(
() => props.secondBoxHeight,
(val) => {
if (props.layout === 'vertical') {
heightUsed.value = 422 + val;
}
},
{
immediate: true,
}
);
const sqlSourceColumns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
isNormal: true,
},
{
title: '',
slotName: 'operation',
width: 50,
},
];
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
activeItem.value.sqlSource.params = [...resultArr];
if (!isInit) {
emit('change');
}
}
</script>
<style lang="less" scoped>
.precondition-content {
@apply flex-1 overflow-y-auto;
.ms-scroll-bar();
padding: 16px;
border: 1px solid rgb(var(--color-text-n8));
border-radius: var(--border-radius-small);
}
.script-name-container {
@apply flex items-center;
margin-right: 16px;
&:hover {
.edit-script-name-icon {
@apply visible;
}
}
.edit-script-name-icon {
@apply invisible cursor-pointer;
color: rgb(var(--primary-5));
}
}
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-table-cell) {
padding: 16px 12px;
}
</style>
<style lang="less" scoped></style>

View File

@ -1,11 +1,7 @@
<template>
<div>
<div class="mb-[8px] flex items-center gap-[8px]">
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear
/>
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestDebug.searchTip')" allow-clear />
<a-dropdown @select="handleSelect">
<a-button type="primary">{{ t('apiTestDebug.newApi') }}</a-button>
<template #content>
@ -17,7 +13,7 @@
<div v-if="!props.isModal" class="folder">
<div class="folder-text">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('caseManagement.caseReview.allReviews') }}</div>
<div class="folder-name">{{ t('apiTestDebug.allRequest') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
</div>
<div class="ml-auto flex items-center">
@ -224,9 +220,9 @@
function deleteFolder(node: MsTreeNodeData) {
openModal({
type: 'error',
title: t('caseManagement.caseReview.deleteFolderTipTitle', { name: node.name }),
content: t('caseManagement.caseReview.deleteFolderTipContent'),
okText: t('caseManagement.caseReview.deleteConfirm'),
title: t('apiTestDebug.deleteFolderTipTitle', { name: node.name }),
content: t('apiTestDebug.deleteFolderTipContent'),
okText: t('apiTestDebug.deleteConfirm'),
okButtonProps: {
status: 'danger',
},
@ -234,7 +230,7 @@
onBeforeOk: async () => {
try {
await deleteReviewModule(node.id);
Message.success(t('caseManagement.caseReview.deleteSuccess'));
Message.success(t('apiTestDebug.deleteSuccess'));
initModules();
} catch (error) {
// eslint-disable-next-line no-console
@ -293,7 +289,7 @@
dropNodeId: dropNode.id || '',
dropPosition,
});
Message.success(t('caseManagement.caseReview.moduleMoveSuccess'));
Message.success(t('apiTestDebug.moduleMoveSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -8,7 +8,7 @@ export default {
'apiTestDebug.header': 'Header',
'apiTestDebug.body': 'Body',
'apiTestDebug.prefix': 'Prefix',
'apiTestDebug.postCondition': 'Post condition',
'apiTestDebug.post': 'Post',
'apiTestDebug.assertion': 'Assertion',
'apiTestDebug.auth': 'Auth',
'apiTestDebug.setting': 'Setting',
@ -24,8 +24,9 @@ export default {
'apiTestDebug.desc': 'Description',
'apiTestDebug.apply': 'Apply',
'apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural',
'apiTestDebug.batchAddParamsTip2':
'Note: Multiple records are separated by newlines. Parameter names in batch addition are repeated. By default, the last data is the latest data.',
'apiTestDebug.batchAddParamsTip2': 'Multiple records are separated by newlines.',
'apiTestDebug.batchAddParamsTip3':
'Parameter names in batch addition are repeated. By default, the last data is the latest data.',
'apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.',
'apiTestDebug.descPlaceholder': 'Please enter content',
};

Some files were not shown because too many files have changed in this diff Show More