feat(接口测试): 接口测试-请求头&参数值输入组件&可增减 tab

This commit is contained in:
baiqi 2023-12-20 10:00:27 +08:00 committed by 刘瑞斌
parent 39a1d0d910
commit 7f2b0ca14a
69 changed files with 4147 additions and 413 deletions

View File

@ -37,7 +37,7 @@
"dependencies": {
"@7polo/kity": "2.0.8",
"@7polo/kityminder-core": "1.4.53",
"@arco-design/web-vue": "^2.52.1",
"@arco-design/web-vue": "^2.53.3",
"@arco-themes/vue-ms-theme-default": "^0.0.30",
"@form-create/arco-design": "^3.1.23",
"@halo-dev/richtext-editor": "0.0.0-alpha.32",

View File

@ -1,7 +1,7 @@
@font-face {
font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1700812414583') format('woff2'), url('iconfont.woff?t=1700812414583') format('woff'),
url('iconfont.ttf?t=1700812414583') format('truetype'), url('iconfont.svg?t=1700812414583#iconfont') format('svg');
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');
}
.iconfont {
font-size: 16px;
@ -10,6 +10,27 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-icon_keyboard::before {
content: '\e796';
}
.icon-icon_split-turn-down-left::before {
content: '\e795';
}
.icon-icon_checkbox1::before {
content: '\e794';
}
.icon-icon_flashlamp::before {
content: '\e793';
}
.icon-icon_clear::before {
content: '\e792';
}
.icon-icon_expand-text-input::before {
content: '\e791';
}
.icon-icon_mock::before {
content: '\e790';
}
.icon-icon_text-wrap-overflow::before {
content: '\e78f';
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,55 @@
"css_prefix_text": "icon-",
"description": "DE、MS项目icon管理",
"glyphs": [
{
"icon_id": "38499852",
"name": "icon_keyboard",
"font_class": "icon_keyboard",
"unicode": "e796",
"unicode_decimal": 59286
},
{
"icon_id": "38496991",
"name": "icon_split-turn-down-left",
"font_class": "icon_split-turn-down-left",
"unicode": "e795",
"unicode_decimal": 59285
},
{
"icon_id": "38462218",
"name": "icon_checkbox",
"font_class": "icon_checkbox1",
"unicode": "e794",
"unicode_decimal": 59284
},
{
"icon_id": "38454074",
"name": "icon_flashlamp",
"font_class": "icon_flashlamp",
"unicode": "e793",
"unicode_decimal": 59283
},
{
"icon_id": "38353463",
"name": "icon_clear",
"font_class": "icon_clear",
"unicode": "e792",
"unicode_decimal": 59282
},
{
"icon_id": "38293413",
"name": "icon_expand-text-input",
"font_class": "icon_expand-text-input",
"unicode": "e791",
"unicode_decimal": 59281
},
{
"icon_id": "38293039",
"name": "icon_mock",
"font_class": "icon_mock",
"unicode": "e790",
"unicode_decimal": 59280
},
{
"icon_id": "38195845",
"name": "icon_text-wrap-overflow",

View File

@ -14,6 +14,20 @@
/>
<missing-glyph />
<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" />
<glyph glyph-name="icon_checkbox1" unicode="&#59284;" d="M832 810.666667A106.666667 106.666667 0 0 0 938.666667 704v-640a106.666667 106.666667 0 0 0-106.666667-106.666667h-640A106.666667 106.666667 0 0 0 85.333333 64v640A106.666667 106.666667 0 0 0 192 810.666667h640z m0-85.333334h-640a21.333333 21.333333 0 0 1-21.333333-21.333333v-640a21.333333 21.333333 0 0 1 21.333333-21.333333h640a21.333333 21.333333 0 0 1 21.333333 21.333333v640a21.333333 21.333333 0 0 1-21.333333 21.333333zM682.666667 597.333333a42.666667 42.666667 0 0 0 42.666666-42.666666v-341.333334a42.666667 42.666667 0 0 0-42.666666-42.666666H341.333333a42.666667 42.666667 0 0 0-42.666666 42.666666V554.666667a42.666667 42.666667 0 0 0 42.666666 42.666666h341.333334z m-42.666667-85.333333H384v-256h256V512z" horiz-adv-x="1024" />
<glyph glyph-name="icon_flashlamp" unicode="&#59283;" d="M512 853.333333c259.2 0 469.333333-210.133333 469.333333-469.333333s-210.133333-469.333333-469.333333-469.333333S42.666667 124.8 42.666667 384 252.8 853.333333 512 853.333333z m0-85.333333a384 384 0 1 1 0-768 384 384 0 0 1 0 768z m-2.261333-132.522667a42.666667 42.666667 0 0 0 19.072-57.216L453.034667 426.666667H640a42.666667 42.666667 0 0 0 40.106667-57.216l-1.962667-4.522667-106.666667-213.333333a42.666667 42.666667 0 0 0-76.288 38.144L570.88 341.333333H384a42.666667 42.666667 0 0 0-40.106667 57.216l1.962667 4.522667 106.666667 213.333333a42.666667 42.666667 0 0 0 57.216 19.072z" horiz-adv-x="1024" />
<glyph glyph-name="icon_clear" unicode="&#59282;" d="M713.6 751.018667l274.432-364.202667a42.666667 42.666667 0 0 0-8.405333-59.733333l-298.410667-224.853334-54.101333-37.632h322.218666a42.666667 42.666667 0 0 0 0-85.333333l-445.312-0.085333H278.016a42.666667 42.666667 0 0 0-34.048 17.024l-55.466667 73.6L51.2 251.904a42.666667 42.666667 0 0 0 8.405333 59.733333l214.4 161.578667 0.426667 0.298667 379.349333 285.866666a42.666667 42.666667 0 0 0 59.733334-8.362666z m-420.693333-370.432L145.066667 269.226667l111.573333-148.053334 42.624-56.618666 189.141333 0.042666 36.693334 29.354667-232.192 286.634667z m378.197333 285.013333l-309.973333-233.557333 232.32-286.890667 37.717333 26.197333 262.997333 198.229334L671.104 665.6z" horiz-adv-x="1024" />
<glyph glyph-name="icon_expand-text-input" unicode="&#59281;" d="M128 384a42.666667 42.666667 0 0 0 42.666667-42.666667v-298.666666h298.666666a42.666667 42.666667 0 0 0 42.368-37.674667L512 0a42.666667 42.666667 0 0 0-42.666667-42.666667H128a42.666667 42.666667 0 0 0-42.666667 42.666667v341.333333a42.666667 42.666667 0 0 0 42.666667 42.666667zM896 810.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-341.333333a42.666667 42.666667 0 0 0-85.333334 0V725.333333h-298.666666a42.666667 42.666667 0 0 0-42.368 37.674667L512 768a42.666667 42.666667 0 0 0 42.666667 42.666667h341.333333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_mock" unicode="&#59280;" d="M789.333333 341.333333a192 192 0 1 0 0-384 192 192 0 0 0 0 384zM896 810.666667a85.333333 85.333333 0 0 0 85.333333-85.333334v-441.045333a235.733333 235.733333 0 0 1-85.333333 74.112V725.333333H128v-640h435.498667c8.96-31.701333 24.405333-60.629333 44.8-85.333333H128a85.333333 85.333333 0 0 0-85.333333 85.333333V725.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h768z m-169.856-554.666667H691.2a8.533333 8.533333 0 0 1-8.533333-8.533333v-196.266667c0-4.693333 3.84-8.533333 8.533333-8.533333h24.448a8.533333 8.533333 0 0 1 8.533333 8.533333v141.056l57.301334-127.914667a8.533333 8.533333 0 0 1 15.616 0l56.576 127.914667V51.2c0-4.693333 3.84-8.533333 8.533333-8.533333h25.258667a8.533333 8.533333 0 0 1 8.533333 8.533333v196.266667a8.533333 8.533333 0 0 1-8.533333 8.533333h-34.944a8.533333 8.533333 0 0 1-7.808-5.077333L789.333333 126.592l-55.381333 124.330667a8.533333 8.533333 0 0 1-7.808 5.077333zM512 640a42.666667 42.666667 0 0 0 42.666667-42.666667v-384a42.666667 42.666667 0 0 0-85.333334 0V597.333333a42.666667 42.666667 0 0 0 42.666667 42.666667zM341.333333 554.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-298.666667a42.666667 42.666667 0 0 0-85.333333 0V512a42.666667 42.666667 0 0 0 42.666666 42.666667z m341.333334-85.333334a42.666667 42.666667 0 0 0 42.666666-42.666666v-51.498667a234.026667 234.026667 0 0 1-85.333333-44.8V426.666667a42.666667 42.666667 0 0 0 42.666667 42.666666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_text-wrap-overflow" unicode="&#59279;" d="M746.666667 488.32a234.666667 234.666667 0 0 0 0-469.333333h-78.208v-52.181334a26.069333 26.069333 0 0 0-41.728-20.821333l-111.232 83.413333a52.138667 52.138667 0 0 0 0 83.456l111.232 83.413334a26.069333 26.069333 0 0 0 41.728-20.864v-52.138667H746.666667a130.389333 130.389333 0 1 1 0 260.736H94.805333a52.138667 52.138667 0 0 0 0 104.32H746.666667zM303.402667 123.264a52.138667 52.138667 0 0 0 0-104.277333H94.805333a52.138667 52.138667 0 0 0 0 104.277333h208.64zM94.805333 853.333333h834.389334a52.138667 52.138667 0 0 0 0-104.277333H94.805333a52.138667 52.138667 0 1 0 0 104.277333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_template_filled" unicode="&#59278;" d="M728.96 853.333333H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-853.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h682.666666a42.666667 42.666667 0 0 1 42.666667 42.666666V686.250667a42.666667 42.666667 0 0 1-12.501333 30.165333l-124.330667 124.416A42.666667 42.666667 0 0 1 729.002667 853.333333zM298.666667 533.333333a21.333333 21.333333 0 0 0 21.333333 21.333334h384a21.333333 21.333333 0 0 0 21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 0-21.333333-21.333334h-384a21.333333 21.333333 0 0 0-21.333333 21.333334v42.666666z m170.666666-170.666666a21.333333 21.333333 0 0 0 21.333334 21.333333h213.333333a21.333333 21.333333 0 0 0 21.333333-21.333333v-213.333334a21.333333 21.333333 0 0 0-21.333333-21.333333h-213.333333a21.333333 21.333333 0 0 0-21.333334 21.333333v213.333334z m-170.666666 0a21.333333 21.333333 0 0 0 21.333333 21.333333h42.666667a21.333333 21.333333 0 0 0 21.333333-21.333333v-213.333334a21.333333 21.333333 0 0 0-21.333333-21.333333h-42.666667a21.333333 21.333333 0 0 0-21.333333 21.333333v213.333334z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 420 KiB

View File

@ -34,6 +34,12 @@
.arco-tabs-tab {
padding: 13px 0 !important;
}
.arco-tabs-nav-button-left {
@apply ml-0 !shadow-none;
}
.arco-tabs-nav-button-right {
@apply mr-0 !shadow-none;
}
/** Modal对话框 **/
.arco-modal {

View File

@ -203,7 +203,7 @@
}
/** 容器内部上下阴影类 **/
.ms-container--shadow() {
.ms-container--shadow-y() {
@apply relative;
&::before {
position: absolute;
@ -234,3 +234,39 @@
box-shadow: inset 0 -10px 6px -10px rgb(0 0 0 / 15%);
}
}
/** 输入框组合-选择器后缀样式类 **/
.ms-input-group--append() {
:deep(.arco-input-append) {
@apply border-none;
}
:deep(.select-input-append) {
@apply z-10;
margin-left: -16px !important;
border-radius: 0 4px 4px 0 !important;
background-color: var(--color-text-n8) !important;
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: var(--color-text-n8) !important;
}
}
}
/** 输入框组合-选择器前缀样式类 **/
.ms-input-group--prepend() {
:deep(.arco-input-prepend) {
@apply border-none;
}
:deep(.select-input-prepend) {
@apply z-10;
margin-right: -16px !important;
border-radius: 4px 0 0 4px !important;
background-color: var(--color-text-n8) !important;
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: var(--color-text-n8) !important;
}
}
}

View File

@ -245,7 +245,7 @@
<style lang="less" scoped>
.ms-card-list-container {
@apply h-full overflow-hidden;
.ms-container--shadow();
.ms-container--shadow-y();
.ms-card-list {
@apply grid max-h-full overflow-auto;

View File

@ -5,7 +5,7 @@
v-model="innerValue"
class="ms-cascader"
:options="props.options"
:trigger-props="{ contentClass: 'ms-cascader-popper' }"
:trigger-props="{ contentClass: `ms-cascader-popper ms-cascader-popper--${props.optionSize}` }"
multiple
allow-clear
check-strictly
@ -13,30 +13,31 @@
:virtual-list-props="props.virtualListProps"
:placeholder="props.placeholder"
:loading="props.loading"
value-key="value"
:value-key="props.valueKey"
:path-mode="props.pathMode"
@change="handleMsCascaderChange"
@clear="clearValues"
>
<template #prefix>
<template v-if="props.prefix" #prefix>
{{ props.prefix }}
</template>
<template #label="{ data }">
<a-tooltip :content="data.label" position="top" :mouse-enter-delay="500" mini>
<div class="one-line-text inline-block">{{ data.label }}</div>
<a-tooltip :content="getInputLabel(data)" position="top" :mouse-enter-delay="500" mini>
<div class="one-line-text inline-block">{{ getInputLabel(data) }}</div>
</a-tooltip>
</template>
<template #option="{ data }">
<a-tooltip :content="data.label" position="top" :mouse-enter-delay="500" mini>
<a-tooltip :content="t(data.label)" position="top" :mouse-enter-delay="500" mini>
<a-radio
v-if="data.level === 0"
v-model:model-value="innerLevel"
:value="data.value.value"
:value="data.value[props.valueKey]"
size="mini"
@change="handleLevelChange"
>
<div class="one-line-text" :style="getOptionComputedStyle">{{ data.label }}</div>
<div class="one-line-text" :style="getOptionComputedStyle">{{ t(data.label) }}</div>
</a-radio>
<div v-else class="one-line-text" :style="getOptionComputedStyle">{{ data.label }}</div>
<div v-else class="one-line-text" :style="getOptionComputedStyle">{{ t(data.label) }}</div>
</a-tooltip>
</template>
</a-cascader>
@ -46,7 +47,7 @@
v-model="innerValue"
class="ms-cascader"
:options="props.options"
:trigger-props="{ contentClass: 'ms-cascader-popper' }"
:trigger-props="{ contentClass: `ms-cascader-popper ms-cascader-popper--${props.optionSize}` }"
:multiple="props.multiple"
allow-clear
:check-strictly="props.strictly"
@ -54,19 +55,30 @@
:placeholder="props.placeholder"
:virtual-list-props="props.virtualListProps"
:loading="props.loading"
:value-key="props.valueKey"
:path-mode="props.pathMode"
@change="(val) => emit('change', val)"
>
<template #prefix>
<template v-if="props.prefix" #prefix>
{{ props.prefix }}
</template>
<template #label="{ data }">
<a-tooltip :content="data.label" position="top" :mouse-enter-delay="500" mini>
<div class="one-line-text inline translate-y-[15%]">{{ data.label }}</div>
</a-tooltip>
<slot name="label" :data="{ ...data, [props.labelKey]: getInputLabel(data) }">
<a-tooltip :content="getInputLabel(data)" position="top" :mouse-enter-delay="500" mini>
<div class="one-line-text inline translate-y-[15%]">
{{ getInputLabel(data) }}
</div>
</a-tooltip>
</slot>
</template>
<template #option="{ data }">
<a-tooltip :content="data.label" position="top" :mouse-enter-delay="500" mini>
<div class="one-line-text" :style="getOptionComputedStyle">{{ data.label }}</div>
</a-tooltip>
<slot name="option" :data="data">
<a-tooltip :content="t(data.label)" position="top" :mouse-enter-delay="500" mini>
<div class="one-line-text" :style="getOptionComputedStyle">
{{ t(data.label) }}
</div>
</a-tooltip>
</slot>
</template>
</a-cascader>
</template>
@ -74,6 +86,7 @@
<script setup lang="ts">
import { Ref, ref, watch } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import useSelect from '@/hooks/useSelect';
import type { CascaderOption } from '@arco-design/web-vue';
@ -94,12 +107,22 @@
panelWidth?: number; // 150px
placeholder?: string;
loading?: boolean;
optionSize?: 'small' | 'default';
pathMode?: boolean; //
valueKey?: string;
labelKey?: string; // labelKey
}
const props = withDefaults(defineProps<MsCascaderProps>(), {
mode: 'MS',
optionSize: 'default',
pathMode: false,
valueKey: 'value',
labelKey: 'label',
});
const emit = defineEmits(['update:modelValue', 'update:level']);
const emit = defineEmits(['update:modelValue', 'update:level', 'change']);
const { t } = useI18n();
const innerValue = ref<CascaderModelValue>([]);
const innerLevel = ref(''); //
@ -190,12 +213,21 @@
calculateMaxTag();
}
// TODO: arco-design cascader path-mode - v-model
function getInputLabel(data: CascaderOption) {
if (!props.pathMode) {
return t(data[props.labelKey].split('-').pop());
}
return t(data[props.labelKey]);
}
function clearValues() {
innerLevel.value = '';
}
</script>
<style lang="less">
/* stylelint-disable value-keyword-case */
.ms-cascader {
@apply overflow-hidden;
.arco-select-view-inner {
@ -208,6 +240,9 @@
.ms-cascader-popper {
.arco-cascader-panel {
.arco-cascader-panel-column {
.arco-cascader-column-content {
padding: 4px 0;
}
.arco-virtual-list {
.ms-scroll-bar();
}
@ -226,5 +261,14 @@
}
}
}
.ms-cascader-popper--small {
.arco-cascader-panel {
.arco-cascader-panel-column {
.arco-cascader-option {
height: 28px;
line-height: 28px;
}
}
}
}
</style>
@/hooks/useSelect

View File

@ -0,0 +1,903 @@
/* eslint-disable no-template-curly-in-string */
import { MockParamItem } from './types';
// mock基础分组
export const mockBaseGroup: MockParamItem[] = [
{
label: 'ms.paramsInput.bool',
value: '@bool',
desc: 'ms.paramsInput.boolDesc',
},
];
// mock数字分组
export const mockNumberGroup: MockParamItem[] = [
{
label: 'ms.paramsInput.natural',
value: '@natural',
desc: 'ms.paramsInput.naturalDesc',
},
{
label: 'ms.paramsInput.naturalRange',
value: '@natural(1,100)',
desc: 'ms.paramsInput.naturalRangeDesc',
inputGroup: [
{
type: 'number',
value: 1,
label: 'ms.paramsInput.min',
placeholder: 'ms.paramsInput.minNaturalPlaceholder',
},
{
type: 'number',
value: 100,
label: 'ms.paramsInput.max',
placeholder: 'ms.paramsInput.maxNaturalPlaceholder',
},
],
},
{
label: 'ms.paramsInput.integer',
value: '@integer',
desc: 'ms.paramsInput.integerDesc',
},
{
label: 'ms.paramsInput.integerRange',
value: '@integer(1,100)',
desc: 'ms.paramsInput.integerRangeDesc',
inputGroup: [
{
type: 'number',
value: 1,
label: 'ms.paramsInput.min',
placeholder: 'ms.paramsInput.minIntegerPlaceholder',
},
{
type: 'number',
value: 100,
label: 'ms.paramsInput.max',
placeholder: 'ms.paramsInput.maxIntegerPlaceholder',
},
],
},
{
label: 'ms.paramsInput.float',
value: '@floatNumber(1,10,2,5)',
desc: 'ms.paramsInput.floatDesc',
inputGroup: [
{
type: 'number',
value: 1,
label: 'ms.paramsInput.floatIntegerMin',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
{
type: 'number',
value: 10,
label: 'ms.paramsInput.floatIntegerMax',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
{
type: 'number',
value: 2,
label: 'ms.paramsInput.floatMin',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
{
type: 'number',
value: 5,
label: 'ms.paramsInput.floatMax',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
],
},
{
label: 'ms.paramsInput.integerArray',
value: '@range(1,100,1)',
desc: 'ms.paramsInput.integerArrayDesc',
inputGroup: [
{
type: 'number',
value: 1,
label: 'start',
placeholder: 'ms.paramsInput.integerArrayStartPlaceholder',
},
{
type: 'number',
value: 100,
label: 'end',
placeholder: 'ms.paramsInput.integerArrayEndPlaceholder',
},
{
type: 'number',
value: 1,
label: 'step',
placeholder: 'ms.paramsInput.integerArrayStepPlaceholder',
},
],
},
];
// mock字符串分组
export const mockStringGroup: MockParamItem[] = [
{
label: 'ms.paramsInput.character',
value: '@character(pool)',
desc: 'ms.paramsInput.characterDesc',
inputGroup: [
{
type: 'input',
value: '',
label: 'ms.paramsInput.character',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
],
},
{
label: 'ms.paramsInput.characterLower',
value: "@character('lower')",
desc: 'ms.paramsInput.characterLowerDesc',
},
{
label: 'ms.paramsInput.characterUpper',
value: "@character('upper')",
desc: 'ms.paramsInput.characterUpperDesc',
},
{
label: 'ms.paramsInput.characterSymbol',
value: "@character('symbol')",
desc: 'ms.paramsInput.characterSymbolDesc',
},
{
label: 'ms.paramsInput.string',
value: '@string(1,10)',
desc: 'ms.paramsInput.stringDesc',
inputGroup: [
{
type: 'number',
value: 1,
label: 'ms.paramsInput.stringMin',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
{
type: 'number',
value: 10,
label: 'ms.paramsInput.stringMax',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
],
},
];
// 字符串变量特殊处理
export const specialStringVars = [
'@character(pool)',
"@character('lower')",
"@character('upper')",
"@character('symbol')",
];
// mock日期分组
export const mockDateGroup: MockParamItem[] = [
{
label: 'ms.paramsInput.date',
value: "@date('yyyy-MM-dd')",
desc: 'ms.paramsInput.dateDesc',
},
{
label: 'ms.paramsInput.time',
value: "@time('HH:mm:ss')",
desc: 'ms.paramsInput.timeDesc',
},
{
label: 'ms.paramsInput.dateTime',
value: "@dateTime('yyyy-MM-dd HH:mm:ss')",
desc: 'ms.paramsInput.dateTimeDesc',
},
{
label: 'ms.paramsInput.nowDateTime',
value: "@now('yyyy-MM-dd HH:mm:ss')",
desc: 'ms.paramsInput.nowDateTimeDesc',
},
];
// mockWeb变量分组
export const mockWebMap: MockParamItem[] = [
{
label: 'ms.paramsInput.url',
value: "@url('http')",
desc: 'ms.paramsInput.urlDesc',
inputGroup: [
{
type: 'inputAppendSelect',
value: '',
label: 'ms.paramsInput.url',
inputValue: '',
selectValue: 'http',
placeholder: 'ms.paramsInput.urlPlaceholder',
options: [
{
label: 'http',
value: 'http',
},
{
label: 'https',
value: 'https',
},
],
},
],
},
{
label: 'ms.paramsInput.protocol',
value: '@protocol',
desc: 'ms.paramsInput.protocolDesc',
},
{
label: 'ms.paramsInput.domain',
value: '@domain',
desc: 'ms.paramsInput.domainDesc',
},
{
label: 'ms.paramsInput.topDomain',
value: '@tld',
desc: 'ms.paramsInput.topDomainDesc',
},
{
label: 'ms.paramsInput.email',
value: '@email',
desc: 'ms.paramsInput.emailDesc',
},
{
label: 'ms.paramsInput.ip',
value: '@ip',
desc: 'ms.paramsInput.ipDesc',
},
];
// mock地区分组
export const mockLocationMap: MockParamItem[] = [
{
label: 'ms.paramsInput.location',
value: '@region',
desc: 'ms.paramsInput.locationDesc',
},
{
label: 'ms.paramsInput.province',
value: '@province',
desc: 'ms.paramsInput.provinceDesc',
},
{
label: 'ms.paramsInput.city',
value: '@city',
desc: 'ms.paramsInput.cityDesc',
},
{
label: 'ms.paramsInput.county',
value: '@county',
desc: 'ms.paramsInput.countyDesc',
},
{
label: 'ms.paramsInput.provinceCityCounty',
value: '@county(true)',
desc: 'ms.paramsInput.provinceCityCountyDesc',
},
{
label: 'ms.paramsInput.zip',
value: '@zip',
desc: 'ms.paramsInput.zipDesc',
},
];
// mock个人信息分组
export const mockPersonalMap: MockParamItem[] = [
{
label: 'ms.paramsInput.idCard',
value: '@idCard',
desc: 'ms.paramsInput.idCardDesc',
},
{
label: 'ms.paramsInput.specifyIdCard',
value: '@idCard(birth)',
desc: 'ms.paramsInput.specifyIdCardDesc',
inputGroup: [
{
type: 'date',
value: '',
label: 'ms.paramsInput.birthday',
placeholder: 'ms.paramsInput.birthdayPlaceholder',
},
],
},
{
label: 'ms.paramsInput.phone',
value: '@phoneNumber',
desc: 'ms.paramsInput.phoneDesc',
},
];
// mock英文名分组
export const mockEnglishNameGroupMap: MockParamItem[] = [
{
label: 'ms.paramsInput.englishName',
value: '@first',
desc: 'ms.paramsInput.englishNameDesc',
},
{
label: 'ms.paramsInput.englishSurname',
value: '@last',
desc: 'ms.paramsInput.englishSurnameDesc',
},
{
label: 'ms.paramsInput.englishFullName',
value: '@name',
desc: 'ms.paramsInput.englishFullNameDesc',
},
];
// mock中文名分组
export const mockChineseNameGroupMap: MockParamItem[] = [
{
label: 'ms.paramsInput.chineseName',
value: '@cfirst',
desc: 'ms.paramsInput.chineseNameDesc',
},
{
label: 'ms.paramsInput.chineseSurname',
value: '@clast',
desc: 'ms.paramsInput.chineseSurnameDesc',
},
{
label: 'ms.paramsInput.chineseFullName',
value: '@cname',
desc: 'ms.paramsInput.chineseFullNameDesc',
},
];
// mock颜色分组
export const mockColorGroupMap: MockParamItem[] = [
{
label: 'ms.paramsInput.color',
value: '@color',
desc: 'ms.paramsInput.colorDesc',
},
{
label: 'RGB',
value: '@rgb',
desc: 'ms.paramsInput.RGBDesc',
},
{
label: 'RGBA',
value: '@rgba',
desc: 'ms.paramsInput.RGBADesc',
},
{
label: 'HSL',
value: '@hsl',
desc: 'ms.paramsInput.hslDesc',
},
];
// mock英文文本分组
export const mockEnglishTextGroupMap: MockParamItem[] = [
{
label: 'ms.paramsInput.englishText',
value: '@paragraph',
desc: 'ms.paramsInput.englishTextDesc',
},
{
label: 'ms.paramsInput.englishSentence',
value: '@sentence',
desc: 'ms.paramsInput.englishSentenceDesc',
},
{
label: 'ms.paramsInput.englishWord',
value: '@word',
desc: 'ms.paramsInput.englishWordDesc',
},
{
label: 'ms.paramsInput.englishTitle',
value: '@title',
desc: 'ms.paramsInput.englishTitleDesc',
},
];
// mock中文文本分组
export const mockChineseTextGroupMap: MockParamItem[] = [
{
label: 'ms.paramsInput.chineseText',
value: '@cparagraph',
desc: 'ms.paramsInput.chineseTextDesc',
},
{
label: 'ms.paramsInput.chineseSentence',
value: '@csentence',
desc: 'ms.paramsInput.chineseSentenceDesc',
},
{
label: 'ms.paramsInput.chineseWord',
value: '@cword',
desc: 'ms.paramsInput.chineseWordDesc',
},
{
label: 'ms.paramsInput.chineseTitle',
value: '@ctitle',
desc: 'ms.paramsInput.chineseTitleDesc',
},
];
// mock正则表达式分组
export const mockRegExpMap: MockParamItem[] = [
{
label: 'ms.paramsInput.regexp',
value: '@regexp',
desc: 'ms.paramsInput.regexpDesc',
inputGroup: [
{
type: 'input',
value: '',
label: 'ms.paramsInput.regexp',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
],
},
];
// mock函数
export const mockFunctions: MockParamItem[] = [
{
label: 'md5',
value: 'md5',
desc: 'ms.paramsInput.md5Desc',
},
{
label: 'base64',
value: 'base64',
desc: 'ms.paramsInput.base64Desc',
},
{
label: 'unbase64',
value: 'unbase64',
desc: 'ms.paramsInput.unbase64Desc',
},
{
label: 'substr',
value: 'substr',
desc: 'ms.paramsInput.substrDesc',
inputGroup: [
{
type: 'number',
value: NaN,
label: 'start',
placeholder: 'ms.paramsInput.substrStartPlaceholder',
},
{
type: 'number',
value: NaN,
label: 'end',
placeholder: 'ms.paramsInput.substrEndPlaceholder',
},
],
},
{
label: 'concatconcat',
value: 'concatconcat',
desc: 'ms.paramsInput.concatconcatDesc',
inputGroup: [
{
type: 'input',
value: '',
label: 'ms.paramsInput.concatconcat',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
],
},
{
label: 'lconcat',
value: 'lconcat',
desc: 'ms.paramsInput.lconcatDesc',
inputGroup: [
{
type: 'input',
value: '',
label: 'ms.paramsInput.lconcat',
placeholder: 'ms.paramsInput.commonPlaceholder',
},
],
},
{
label: 'sha1',
value: 'sha1',
desc: 'ms.paramsInput.sha1Desc',
},
{
label: 'sha224',
value: 'sha224',
desc: 'ms.paramsInput.sha224Desc',
},
{
label: 'sha256',
value: 'sha256',
desc: 'ms.paramsInput.sha256Desc',
},
{
label: 'sha384',
value: 'sha384',
desc: 'ms.paramsInput.sha384Desc',
},
{
label: 'sha512',
value: 'sha512',
desc: 'ms.paramsInput.sha512Desc',
},
{
label: 'lower',
value: 'lower',
desc: 'ms.paramsInput.lowerDesc',
},
{
label: 'upper',
value: 'upper',
desc: 'ms.paramsInput.upperDesc',
},
{
label: 'length',
value: 'length',
desc: 'ms.paramsInput.lengthDesc',
},
{
label: 'number',
value: 'number',
desc: 'ms.paramsInput.numberDesc',
},
];
// mock所有变量
export const mockAllParams: MockParamItem[] = [
...mockBaseGroup,
...mockNumberGroup,
...mockStringGroup,
...mockDateGroup,
...mockWebMap,
...mockLocationMap,
...mockPersonalMap,
...mockEnglishNameGroupMap,
...mockChineseNameGroupMap,
...mockColorGroupMap,
...mockEnglishTextGroupMap,
...mockChineseTextGroupMap,
...mockRegExpMap,
];
// mock所有分组
export const mockAllGroup = [
{
value: 'base',
label: 'ms.paramsInput.base',
children: mockBaseGroup,
},
{
value: 'string',
label: 'ms.paramsInput.string',
children: mockStringGroup,
},
{
value: 'personal',
label: 'ms.paramsInput.personal',
children: mockPersonalMap,
},
{
value: 'dateTime',
label: 'ms.paramsInput.dateOrTime',
children: mockDateGroup,
},
{
value: 'cname',
label: 'ms.paramsInput.chineseFullName',
children: mockChineseNameGroupMap,
},
{
value: 'ename',
label: 'ms.paramsInput.englishFullName',
children: mockEnglishNameGroupMap,
},
{
value: 'ctext',
label: 'ms.paramsInput.cText',
children: mockChineseTextGroupMap,
},
{
value: 'etext',
label: 'ms.paramsInput.eText',
children: mockEnglishTextGroupMap,
},
{
value: 'web',
label: 'ms.paramsInput.web',
children: mockWebMap,
},
{
value: 'number',
label: 'ms.paramsInput.number',
children: mockNumberGroup,
},
{
value: 'location',
label: 'ms.paramsInput.county',
children: mockLocationMap,
},
{
value: 'color',
label: 'ms.paramsInput.color',
children: mockColorGroupMap,
},
{
value: 'regexp',
label: 'ms.paramsInput.regexp',
children: mockRegExpMap,
},
];
// JMeter变量分组
export const JMeterVariableGroup = [
{
label: 'ms.paramsInput.randomFromMultipleVars',
value: '${__RandomFromMultipleVars}',
},
{
label: 'ms.paramsInput.split',
value: '${__split}',
},
{
label: 'ms.paramsInput.eval',
value: '${__eval}',
},
{
label: 'ms.paramsInput.evalVar',
value: '${__evalVar}',
},
{
label: 'ms.paramsInput.V',
value: '${__V}',
},
];
// JMeter编码分组
export const JMeterCodeGroup = [
{
label: 'ms.paramsInput.escapeHtml',
value: '${__escapeHtml}',
},
{
label: 'ms.paramsInput.escapeXml',
value: '${__escapeXml}',
},
{
label: 'ms.paramsInput.unescape',
value: '${__unescape}',
},
{
label: 'ms.paramsInput.unescapeHtml',
value: '${__unescapeHtml}',
},
{
label: 'ms.paramsInput.urldecode',
value: '${__urldecode}',
},
{
label: 'ms.paramsInput.urlencode',
value: '${__urlencode}',
},
];
// JMeter脚本分组
export const JMeterScriptGroup = [
{
label: 'ms.paramsInput.groovy',
value: '${__groovy}',
},
{
label: 'ms.paramsInput.BeanShell',
value: '${__BeanShell}',
},
{
label: 'ms.paramsInput.javaScript',
value: '${__javaScript}',
},
{
label: 'ms.paramsInput.jexl2',
value: '${__jexl2}',
},
{
label: 'ms.paramsInput.jexl3',
value: '${__jexl3}',
},
];
// JMeter时间分组
export const JMeterTimeGroup = [
{
label: 'ms.paramsInput.jmeterTime',
value: '${__time}',
},
{
label: 'ms.paramsInput.timeShift',
value: '${__timeShift}',
},
{
label: 'ms.paramsInput.dateTimeConvert',
value: '${__dateTimeConvert}',
},
{
label: 'ms.paramsInput.RandomDate',
value: '${__RandomDate}',
},
];
// JMeter属性分组
export const JMeterPropertyGroup = [
{
label: 'ms.paramsInput.isPropDefined',
value: '${__isPropDefined}',
},
{
label: 'ms.paramsInput.readProperty',
value: '${__property}',
},
{
label: 'ms.paramsInput.P',
value: '${__P}',
},
{
label: 'ms.paramsInput.setProperty',
value: '${__setProperty}',
},
{
label: 'ms.paramsInput.isVarDefined',
value: '${__isVarDefined}',
},
];
// JMeter数字分组
export const JMeterNumberGroup = [
{
label: 'ms.paramsInput.counter',
value: '${__counter}',
},
{
label: 'ms.paramsInput.intSum',
value: '${__intSum}',
},
{
label: 'ms.paramsInput.longSum',
value: '${__longSum}',
},
{
label: 'ms.paramsInput.Random',
value: '${__Random}',
},
];
// JMeter文件分组
export const JMeterFileGroup = [
{
label: 'ms.paramsInput.StringFromFile',
value: '${__StringFromFile}',
},
{
label: 'ms.paramsInput.FileToString',
value: '${__FileToString}',
},
{
label: 'ms.paramsInput.CSVRead',
value: '${__CSVRead}',
},
{
label: 'ms.paramsInput.XPath',
value: '${__XPath}',
},
{
label: 'ms.paramsInput.StringToFile',
value: '${__StringToFile}',
},
];
// JMeter信息分组
export const JMeterInfoGroup = [
{
label: 'ms.paramsInput.digest',
value: '${__digest}',
},
{
label: 'ms.paramsInput.threadNum',
value: '${__threadNum}',
},
{
label: 'ms.paramsInput.threadGroupName',
value: '${__threadGroupName}',
},
{
label: 'ms.paramsInput.samplerName',
value: '${__samplerName}',
},
{
label: 'ms.paramsInput.machineIP',
value: '${__machineIP}',
},
{
label: 'ms.paramsInput.machineName',
value: '${__machineName}',
},
{
label: 'ms.paramsInput.TestPlanName',
value: '${__TestPlanName}',
},
];
// JMeter字符串分组
export const JMeterStringGroup = [
{
label: 'ms.paramsInput.log',
value: '${__log}',
},
{
label: 'ms.paramsInput.logn',
value: '${__logn}',
},
{
label: 'ms.paramsInput.RandomString',
value: '${__RandomString}',
},
{
label: 'ms.paramsInput.UUID',
value: '${__UUID}',
},
{
label: 'ms.paramsInput.char',
value: '${__char}',
},
{
label: 'ms.paramsInput.changeCase',
value: '${__changeCase}',
},
{
label: 'ms.paramsInput.regexFunction',
value: '${__regexFunction}',
},
];
// JMeter所有分组
export const JMeterAllGroup = [
{
value: 'variable',
label: 'ms.paramsInput.variable',
children: JMeterVariableGroup,
},
{
value: 'code',
label: 'ms.paramsInput.code',
children: JMeterCodeGroup,
},
{
value: 'script',
label: 'ms.paramsInput.script',
children: JMeterScriptGroup,
},
{
value: 'time',
label: 'ms.paramsInput.time',
children: JMeterTimeGroup,
},
{
value: 'property',
label: 'ms.paramsInput.property',
children: JMeterPropertyGroup,
},
{
value: 'number',
label: 'ms.paramsInput.number',
children: JMeterNumberGroup,
},
{
value: 'file',
label: 'ms.paramsInput.file',
children: JMeterFileGroup,
},
{
value: 'info',
label: 'ms.paramsInput.info',
children: JMeterInfoGroup,
},
{
value: 'string',
label: 'ms.paramsInput.string',
children: JMeterStringGroup,
},
];
// JMeter所有变量
export const JMeterAllVars = [
...JMeterVariableGroup,
...JMeterCodeGroup,
...JMeterScriptGroup,
...JMeterTimeGroup,
...JMeterPropertyGroup,
...JMeterNumberGroup,
...JMeterFileGroup,
...JMeterInfoGroup,
...JMeterStringGroup,
];

View File

@ -0,0 +1,642 @@
<template>
<a-trigger
v-model:popup-visible="paramSettingVisible"
trigger="click"
:popup-translate="[0, 16]"
position="bl"
class="ms-params-input-setting-trigger"
>
<span class="invisible"></span>
<template #content>
<div class="ms-params-input-setting-trigger-content">
<div class="mb-[16px] flex items-center justify-between">
<div class="font-semibold text-[var(--color-text-1)]">{{ t('ms.paramsInput.paramSetting') }}</div>
<a-radio-group
v-model:model-value="paramSettingType"
type="button"
size="small"
@change="handleParamSettingChange"
>
<a-radio value="mock">Mock</a-radio>
<a-radio value="jmeter">JMeter</a-radio>
</a-radio-group>
</div>
<div class="ms-params-input-setting-trigger-content-scroll">
<a-form ref="paramFormRef" :model="paramForm" layout="vertical">
<template v-if="paramSettingType === 'mock'">
<a-form-item
field="type"
:label="t('ms.paramsInput.mockType')"
:rules="[{ required: true, message: t('ms.paramsInput.mockTypePlaceholder') }]"
asterisk-position="end"
class="mb-[16px]"
>
<MsCascader
v-model:model-value="paramForm.type"
mode="native"
:options="paramTypeOptions"
:placeholder="t('ms.paramsInput.mockTypePlaceholder')"
option-size="small"
label-key="value"
value-key="key"
@change="handleParamTypeChange"
>
<template #label="{ data }">
<a-tooltip :content="`${t(data.label.split('/').pop().trim())} ${paramForm.type}`">
<div class="one-line-text inline-flex w-full items-center justify-between pr-[8px]" title="">
{{ t(data.label.split('/').pop().trim()) }}
<div class="max-w-[60%] text-[var(--color-text-4)]" title="">
{{ paramForm.type }}
</div>
</div>
</a-tooltip>
</template>
<template #option="{ data }">
<div
:class="`flex ${data.isLeaf ? 'mr-[-26px] w-[270px]' : 'w-[120px]'} items-center justify-between`"
title=""
>
<a-tooltip :content="t(data.label)">
<div :class="`one-line-text ${data.isLeaf ? 'max-w-[50%]' : ''}`" title="">
{{ t(data.label) }}
</div>
</a-tooltip>
<a-tooltip v-if="data.isLeaf" :content="data.value">
<div class="one-line-text max-w-[50%] text-[var(--color-text-4)]">
{{ data.value }}
</div>
</a-tooltip>
</div>
</template>
</MsCascader>
</a-form-item>
<paramsInputGroup
v-if="currentParamsInputGroup.length > 0"
:param-form="paramForm"
:input-group="currentParamsInputGroup"
/>
<a-form-item :label="t('ms.paramsInput.addFunc')" class="mb-[16px]">
<a-select
v-model:model-value="paramForm.func"
:options="paramFuncOptions"
:placeholder="t('ms.paramsInput.commonSelectPlaceholder')"
@change="(val) => handleParamFuncChange(val as string)"
>
<template #label="{ data }">
<a-tooltip :content="`${t(data.label)} ${t(data.desc)}`">
<div class="one-line-text inline-flex w-full items-center justify-between pr-[8px]" title="">
{{ data.value }}
<div class="max-w-[60%] text-[var(--color-text-4)]" title="">
{{ t(data.desc) }}
</div>
</div>
</a-tooltip>
</template>
<template #option="{ data }">
<div class="flex w-[420px] items-center justify-between">
{{ t(data.label) }}
<a-tooltip :content="t(data.desc)">
<div class="one-line-text max-w-[70%] text-[var(--color-text-4)]">
{{ t(data.desc) }}
</div>
</a-tooltip>
</div>
</template>
</a-select>
</a-form-item>
<paramsInputGroup
v-if="currentParamsFuncInputGroup.length > 0"
type="func"
:param-form="paramForm"
:input-group="currentParamsFuncInputGroup"
/>
</template>
<template v-else>
<a-form-item
field="JMeterType"
:label="t('ms.paramsInput.jmeterType')"
:rules="[{ required: true, message: t('ms.paramsInput.mockTypePlaceholder') }]"
asterisk-position="end"
class="mb-[16px]"
>
<MsCascader
v-model:model-value="paramForm.JMeterType"
mode="native"
:options="JMeterVarsOptions"
:placeholder="t('ms.paramsInput.mockTypePlaceholder')"
option-size="small"
label-key="value"
value-key="key"
@change="handleJMeterTypeChange"
>
<template #label="{ data }">
<div class="inline-flex w-full items-center justify-between">
<a-tooltip :content="t(data.label.split('/').pop().trim())">
<div class="one-line-text max-w-[50%]" title="">
{{ t(data.label.split('/').pop().trim()) }}
</div>
</a-tooltip>
<a-tooltip :content="`${t(data.label.split('/').pop().trim())} ${paramForm.JMeterType}`">
<div class="max-w-[50%] text-[var(--color-text-4)]" title="">
{{ paramForm.JMeterType }}
</div>
</a-tooltip>
</div>
</template>
<template #option="{ data }">
<div
:class="`flex ${data.isLeaf ? 'mr-[-26px] w-[270px]' : 'w-[120px]'} items-center justify-between`"
>
<a-tooltip :content="t(data.label)">
<div :class="`one-line-text ${data.isLeaf ? 'max-w-[50%]' : ''}`" title="">
{{ t(data.label) }}
</div>
</a-tooltip>
<a-tooltip v-if="data.isLeaf" :content="data.value">
<div class="one-line-text max-w-[50%] text-[var(--color-text-4)]">
{{ data.value }}
</div>
</a-tooltip>
</div>
</template>
</MsCascader>
</a-form-item>
</template>
</a-form>
<div class="mb-[16px] flex items-center gap-[16px] bg-[var(--color-text-n9)] p-[5px_8px]">
<div class="text-[var(--color-text-3)]">{{ t('ms.paramsInput.preview') }}</div>
<div class="text-[var(--color-text-1)]">{{ paramPreview }}</div>
</div>
</div>
<div class="flex items-center justify-end gap-[8px]">
<a-button type="secondary" size="mini" @click="cancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" size="mini" @click="apply">{{ t('ms.paramsInput.apply') }}</a-button>
</div>
</div>
</template>
</a-trigger>
<a-popover
v-model:popup-visible="popoverVisible"
position="tl"
:disabled="disabledPopover"
class="ms-params-input-popover"
>
<template #content>
<div class="ms-params-popover-title !mb-[8px]">
{{ t('ms.paramsInput.value') }}
</div>
<div class="ms-params-popover-subtitle">
{{ t('ms.paramsInput.value') }}
</div>
<div class="ms-params-popover-value mb-[8px]">
{{ innerValue }}
</div>
<div class="ms-params-popover-subtitle">
{{ t('ms.paramsInput.preview') }}
</div>
<div class="ms-params-popover-value">
{{ innerValue }}
</div>
</template>
<a-auto-complete
ref="autoCompleteRef"
v-model:model-value="innerValue"
:data="autoCompleteParams"
:placeholder="t('ms.paramsInput.placeholder', { at: '@' })"
:class="`ms-params-input ${paramSettingVisible ? 'ms-params-input--focus' : ''}`"
:trigger-props="{ contentClass: 'ms-params-input-trigger' }"
:filter-option="false"
@search="handleSearchParams"
@change="(val) => emit('change', val)"
@select="selectAutoComplete"
>
<template #suffix>
<MsIcon type="icon-icon_mock" class="ms-params-input-mock-icon" @click.stop="openParamSetting" />
</template>
<template #option="{ data }">
<div class="w-[350px]">
{{ data.raw.value }}
<a-tooltip :content="t(data.raw.desc)" position="bl" :mouse-enter-delay="300">
<div class="one-line-text max-w-[320px] text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t(data.raw.desc) }}
</div>
</a-tooltip>
</div>
</template>
</a-auto-complete>
</a-popover>
</template>
<script setup lang="ts">
import { useEventListener, useVModel } from '@vueuse/core';
import { cloneDeep } from 'lodash-es';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsCascader from '@/components/business/ms-cascader/index.vue';
import paramsInputGroup from './paramsInputGroup.vue';
import { useI18n } from '@/hooks/useI18n';
import {
JMeterAllGroup,
JMeterAllVars,
mockAllGroup,
mockAllParams,
mockFunctions,
specialStringVars,
} from './config';
import type { MockParamInputGroupItem, MockParamItem } from './types';
import type { AutoComplete, CascaderOption, FormInstance } from '@arco-design/web-vue';
const props = defineProps<{
value: string;
}>();
const emit = defineEmits<{
(e: 'update:value', val: string): void;
(e: 'change', val: string): void;
(e: 'dblclick'): void;
(e: 'apply', val: string): void;
}>();
const { t } = useI18n();
const innerValue = useVModel(props, 'value', emit);
const autoCompleteParams = ref<MockParamItem[]>([]);
const isFocusAutoComplete = ref(false);
const popoverVisible = ref(false);
const lastTenParams = ref<MockParamItem[]>(JSON.parse(localStorage.getItem('ms-lastTenParams') || '[]')); // 使 10
/**
* 搜索变量
* @param val 变量名
*/
function handleSearchParams(val: string) {
if (val === '@') {
autoCompleteParams.value = [...lastTenParams.value];
if (autoCompleteParams.value.length < 10) {
// 使 10
let lastLength = 10 - autoCompleteParams.value.length; //
for (let i = 0; i < mockAllParams.length; i++) {
const mockParam = mockAllParams[i];
if (!autoCompleteParams.value.find((e) => e.value === mockParam.value)) {
//
autoCompleteParams.value.push(mockParam);
lastLength--;
}
if (lastLength === 0) {
break;
}
}
}
} else if (/^@/.test(val)) {
autoCompleteParams.value = mockAllParams.filter((e) => {
return e.value.includes(val);
});
} else {
autoCompleteParams.value = [];
}
}
/**
* 设置最近使用的前 10 个变量
* @param val 变量名
*/
function setLastTenParams(val: string) {
const index = lastTenParams.value.findIndex((e) => e.value === val);
const lastParamsItem = lastTenParams.value.find((e) => e.value === val);
if (index > -1 && lastParamsItem) {
//
lastTenParams.value.splice(index, 1, lastParamsItem);
} else {
//
const mockParamItem = mockAllParams.find((e) => e.value === val);
if (mockParamItem) {
lastTenParams.value.unshift(mockParamItem);
if (lastTenParams.value.length > 10) {
// 10
lastTenParams.value.pop();
}
}
}
localStorage.setItem('ms-lastTenParams', JSON.stringify(lastTenParams.value));
}
function selectAutoComplete(val: string) {
innerValue.value = val;
setLastTenParams(val);
}
const autoCompleteRef = ref<InstanceType<typeof AutoComplete>>();
onMounted(() => {
useEventListener(autoCompleteRef.value?.inputRef, 'dblclick', () => {
emit('dblclick');
});
const autoCompleteInput = (autoCompleteRef.value?.inputRef as any)?.$el.querySelector('.arco-input');
useEventListener(autoCompleteInput, 'focus', () => {
isFocusAutoComplete.value = true;
popoverVisible.value = false;
});
useEventListener(autoCompleteInput, 'blur', () => {
isFocusAutoComplete.value = false;
});
});
const disabledPopover = computed(() => {
return !innerValue.value || innerValue.value.trim() === '' || isFocusAutoComplete.value;
});
const paramSettingVisible = ref(false);
const paramSettingType = ref<'mock' | 'jmeter'>('mock');
const defaultParamForm = {
type: '',
JMeterType: '',
param1: '',
param2: '',
param3: '',
param4: '',
func: '',
funcParam1: '',
funcParam2: '',
};
const paramForm = ref<Record<string, any>>({ ...defaultParamForm });
const paramFormRef = ref<FormInstance>();
const paramTypeOptions: CascaderOption[] = cloneDeep(mockAllGroup);
const paramFuncOptions: MockParamItem[] = cloneDeep(mockFunctions);
const paramPreview = ref('xsxsxsxs');
const currentParamsInputGroup = ref<MockParamInputGroupItem[]>([]);
/**
* 切换变量类型设置变量输入框的输入组
* @param val 变量类型
*/
function handleParamTypeChange(val: string) {
paramForm.value.type = val;
// @
const regex = /@([a-zA-Z]+)(\([^)]*\))?/;
const currentParamType = mockAllParams.find((e) => {
if (specialStringVars.includes(val)) {
return e.value === val;
}
if (e.value.match(regex)?.[1] === val.match(regex)?.[1]) {
paramForm.value.type = e.value;
return true;
}
return false;
});
if (currentParamType) {
currentParamsInputGroup.value = currentParamType.inputGroup || [];
} else {
currentParamsInputGroup.value = [];
}
paramForm.value = {
...paramForm.value,
param1: '',
param2: '',
param3: '',
param4: '',
};
}
function handleJMeterTypeChange(val: string) {
paramForm.value.JMeterType = val;
}
const currentParamsFuncInputGroup = ref<MockParamInputGroupItem[]>([]);
/**
* 切换函数设置函数输入框的输入组
* @param val 函数
*/
function handleParamFuncChange(val: string) {
paramForm.value.func = val;
const currentParamFunc = mockFunctions.find((e) => e.value === val);
if (currentParamFunc) {
currentParamsFuncInputGroup.value = cloneDeep(currentParamFunc.inputGroup) || [];
} else {
currentParamsFuncInputGroup.value = [];
}
paramForm.value = {
...paramForm.value,
funcParam1: '',
funcParam2: '',
};
}
/**
* 打开变量设置弹窗
*/
function openParamSetting() {
if (/^\$/.test(innerValue.value)) {
paramSettingType.value = 'jmeter';
if (JMeterAllVars.findIndex((e) => e.value === innerValue.value) !== -1) {
paramForm.value.JMeterType = innerValue.value;
} else {
paramForm.value.JMeterType = '';
}
} else if (/^@/.test(innerValue.value)) {
const valueArr = innerValue.value.split('|'); // mock
if (valueArr[0]) {
// @
const variableRegex = /@([a-zA-Z]+)(?:\(([^)]*)\))?/;
const variableMatch = valueArr[0].match(variableRegex);
if (variableMatch) {
const variableName = variableMatch[1];
const variableParams = variableMatch[2]?.split(',').map((param) => param.trim());
if (variableName === 'character') {
handleParamTypeChange(`@${variableName}(${variableParams})`); // character
} else {
handleParamTypeChange(`@${variableName}`); //
}
if (variableName !== 'character' || (variableName === 'character' && variableParams?.[0] !== 'pool')) {
// @character(pool) pool
(variableParams || []).forEach((e, i) => {
//
paramForm.value[`param${i + 1}`] = Number.isNaN(Number(e)) ? e : Number(e);
});
}
}
}
if (valueArr[1]) {
//
const functionRegex = /([a-zA-Z]+)(?:\(([^)]*)\))?/;
const functionMatch = valueArr[1].match(functionRegex);
if (functionMatch) {
const functionName = functionMatch[1];
const functionParams = functionMatch[2]?.split(',').map((param) => param.trim());
handleParamFuncChange(functionName); //
(functionParams || []).forEach((e, i) => {
//
paramForm.value[`funcParam${i + 1}`] = Number.isNaN(Number(e)) ? e : Number(e);
});
}
}
}
paramSettingVisible.value = true;
}
function handleParamSettingChange() {
paramFormRef.value?.clearValidate();
}
const JMeterVarsOptions: CascaderOption[] = cloneDeep(JMeterAllGroup);
function cancel() {
paramFormRef.value?.resetFields();
paramSettingType.value = 'mock';
paramSettingVisible.value = false;
currentParamsInputGroup.value = [];
currentParamsFuncInputGroup.value = [];
paramForm.value = { ...defaultParamForm };
}
/**
* 应用 Mock类型变量和函数
*/
function applyMock() {
let resultStr = '';
//
if (currentParamsInputGroup.value.length > 0) {
const testReg = /\(([^)]+)\)/;
const paramVal = [paramForm.value.param1, paramForm.value.param2, paramForm.value.param3, paramForm.value.param4]
.filter((e) => e !== '')
.join(',');
// ()
resultStr = paramVal !== '' ? paramForm.value.type.replace(testReg, `(${paramVal})`) : paramForm.value.type;
} else {
resultStr = paramForm.value.type;
}
if (paramForm.value.func !== '') {
//
resultStr = `${resultStr}|${paramForm.value.func}`;
if (currentParamsFuncInputGroup.value.length > 0 && !Number.isNaN(paramForm.value.funcParam1)) {
//
resultStr = `${resultStr}(${[paramForm.value.funcParam1, paramForm.value.funcParam2]
.filter((e) => !Number.isNaN(e))
.join(',')})`;
}
}
return resultStr;
}
function apply() {
paramFormRef.value?.validate((errors) => {
if (!errors) {
let result = '';
if (paramSettingType.value === 'mock') {
result = applyMock();
} else {
result = paramForm.value.JMeterType;
}
setLastTenParams(paramForm.value.type);
innerValue.value = result;
emit('apply', result);
cancel();
}
});
}
</script>
<style lang="less">
.ms-params-input-popover {
.arco-trigger-popup-wrapper {
.arco-popover-popup-content {
padding: 4px 8px;
}
}
}
.ms-params-input-setting-trigger {
@apply bg-white;
.ms-params-input-setting-trigger-content {
padding: 16px;
width: 480px;
border-radius: var(--border-radius-medium);
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 10%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 3px 14px 2px rgb(0 0 0 / 5%);
&::before {
@apply absolute left-0 top-0;
content: '';
z-index: -1;
width: 200%;
height: 200%;
border: 1px solid var(--color-text-input-border);
border-radius: 12px;
transform-origin: 0 0;
transform: scale(0.5, 0.5);
}
.ms-params-input-setting-trigger-content-scroll {
.ms-scroll-bar();
overflow-y: auto;
margin-right: -6px;
max-height: 400px;
}
}
}
.ms-params-input:not(.arco-input-focus) {
border-color: transparent;
&:not(:hover) {
border-color: transparent;
}
}
.ms-params-input {
.ms-params-input-mock-icon {
@apply invisible;
}
&:hover,
&.arco-input-focus {
.ms-params-input-mock-icon {
@apply visible cursor-pointer;
&:hover {
color: rgb(var(--primary-5));
}
}
}
:deep(.arco-select-option) {
@apply flex flex-1 p-10;
}
}
.ms-params-input--focus {
border-color: rgb(var(--primary-5)) !important;
.ms-params-input-mock-icon {
@apply visible cursor-pointer;
color: rgb(var(--primary-5));
}
}
.ms-params-input-trigger {
width: 350px;
.arco-select-dropdown-list {
.arco-select-option {
@apply !h-auto;
padding: 2px 8px !important;
}
}
}
.ms-params-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.ms-params-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.ms-params-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,219 @@
export default {
'ms.paramsInput.value': 'Parameter value',
'ms.paramsInput.placeholder': 'Starting with {at}, double-click to quickly enter',
'ms.paramsInput.preview': 'Parameter preview',
'ms.paramsInput.natural': 'Natural number',
'ms.paramsInput.naturalDesc': 'Returns a random natural number',
'ms.paramsInput.naturalRange': 'Natural numbers from 1-100',
'ms.paramsInput.naturalRangeDesc':
'Returns a random natural number from 1 to 100 (an integer greater than or equal to 1)',
'ms.paramsInput.max': 'Maximum',
'ms.paramsInput.maxNaturalPlaceholder': 'Enter an integer <=100, such as 100',
'ms.paramsInput.min': 'Minimum',
'ms.paramsInput.minNaturalPlaceholder': 'Enter an integer >=1, such as 1',
'ms.paramsInput.integer': 'Integer',
'ms.paramsInput.integerDesc': 'Returns a random integer',
'ms.paramsInput.integerRange': 'Integer from 1-100',
'ms.paramsInput.integerRangeDesc': 'Returns a random integer between 1 and 100',
'ms.paramsInput.maxIntegerPlaceholder': 'Enter an integer, such as 100, the maximum value cannot exceed 100',
'ms.paramsInput.minIntegerPlaceholder': 'Enter an integer, such as 1',
'ms.paramsInput.float': 'Float',
'ms.paramsInput.floatDesc':
'Returns a random floating point number, integer 1-10, minimum value 2, maximum value 5 in the decimal part.',
'ms.paramsInput.floatMin': 'Minimum decimal part',
'ms.paramsInput.floatMax': 'Maximum decimal part',
'ms.paramsInput.floatIntegerMin': 'Minimum value of integer part',
'ms.paramsInput.floatIntegerMax': 'Maximum value of integer part',
'ms.paramsInput.commonPlaceholder': 'Please input',
'ms.paramsInput.commonSelectPlaceholder': 'Please select',
'ms.paramsInput.bool': 'Boolean',
'ms.paramsInput.boolDesc': 'Generate a random boolean value',
'ms.paramsInput.character': 'Character pool',
'ms.paramsInput.characterDesc': 'Return random characters from the character pool',
'ms.paramsInput.characterLower': 'Random lowercase characters',
'ms.paramsInput.characterLowerDesc': 'Returns a random lowercase character',
'ms.paramsInput.characterUpper': 'Random uppercase characters',
'ms.paramsInput.characterUpperDesc': 'Returns a random uppercase character',
'ms.paramsInput.characterSymbol': 'Special symbol',
'ms.paramsInput.characterSymbolDesc': 'Returns a random special symbol',
'ms.paramsInput.characterFormat': 'Format',
'ms.paramsInput.string': 'String',
'ms.paramsInput.stringDesc': 'Returns a random string from the string pool with 1-10 characters.',
'ms.paramsInput.stringMin': 'Minimum character count',
'ms.paramsInput.stringMax': 'Maximum character count',
'ms.paramsInput.integerArray': 'Integer array',
'ms.paramsInput.integerArrayDesc':
'Return an integer array, the parameters are: start: starting value, stop: end value, step: step size',
'ms.paramsInput.integerArrayStartPlaceholder': 'Start value',
'ms.paramsInput.integerArrayEndPlaceholder': 'End value',
'ms.paramsInput.integerArrayStepPlaceholder': 'Step size',
'ms.paramsInput.date': 'Date',
'ms.paramsInput.dateDesc': 'Returns a random date string. Example: 1983-01-29',
'ms.paramsInput.time': 'Time',
'ms.paramsInput.timeDesc': 'Returns a random time string. Example: 20:47:37',
'ms.paramsInput.dateTime': 'Date and time',
'ms.paramsInput.dateTimeDesc': 'Returns a random date and time string. Example: 1977-11-17 03:50:15',
'ms.paramsInput.nowDateTime': 'Current date and time',
'ms.paramsInput.nowDateTimeDesc': 'Returns the current date string. Example: 2014-04-29 20:08:38',
'ms.paramsInput.uuidDesc': 'Randomly generate a UUID. Example: eFD616Bd-e149-c98E-a041-5e12ED0C94Fd',
'ms.paramsInput.url': 'URL',
'ms.paramsInput.urlDesc': 'Randomly generate an http URL',
'ms.paramsInput.urlPlaceholder': 'Please enter URL',
'ms.paramsInput.protocol': 'Protocol',
'ms.paramsInput.protocolDesc': 'Randomly generate a URL protocol. Example: http ftp',
'ms.paramsInput.domain': 'Domain',
'ms.paramsInput.domainDesc': 'Generate a domain name randomly',
'ms.paramsInput.topDomain': 'Top level domain',
'ms.paramsInput.topDomainDesc': 'Randomly generate a top-level domain name. Example: net',
'ms.paramsInput.email': 'Email address',
'ms.paramsInput.emailDesc': 'Randomly generate an email address',
'ms.paramsInput.ip': 'IP address',
'ms.paramsInput.ipDesc': 'Randomly generate an IP address',
'ms.paramsInput.location': 'Area',
'ms.paramsInput.locationDesc': 'Randomly generate a (China) region. Example: North China',
'ms.paramsInput.province': 'Province',
'ms.paramsInput.provinceDesc':
'Randomly generate a (China) province (or municipality, autonomous region, special administrative region)',
'ms.paramsInput.city': 'City',
'ms.paramsInput.cityDesc': 'Randomly generate a (China) city',
'ms.paramsInput.county': 'County',
'ms.paramsInput.countyDesc': 'Randomly generate a (China) county',
'ms.paramsInput.provinceCityCounty': 'Province/City/County',
'ms.paramsInput.provinceCityCountyDesc':
'Randomly generate a (China) county (with province and city). Example: Huining County, Baiyin City, Gansu Province',
'ms.paramsInput.zip': 'Zip code',
'ms.paramsInput.zipDesc': 'Randomly generate a zip code',
'ms.paramsInput.idCard': 'ID number',
'ms.paramsInput.idCardDesc': 'Randomly generate an ID number (the generated ID number may not be true and valid)',
'ms.paramsInput.specifyIdCard': 'ID number of specified month',
'ms.paramsInput.specifyIdCardDesc':
'Randomly generate an ID number (birthday can be specified) (the generated ID number may not be true and valid)',
'ms.paramsInput.birthday': 'Birth date',
'ms.paramsInput.birthdayPlaceholder': 'Please select a date',
'ms.paramsInput.phone': 'Phone number',
'ms.paramsInput.phoneDesc':
'Randomly generate a mobile phone number (the generated mobile phone number may not be real and valid)',
'ms.paramsInput.englishName': 'English name',
'ms.paramsInput.englishNameDesc': 'Randomly generate a common English name',
'ms.paramsInput.englishSurname': 'English surname',
'ms.paramsInput.englishSurnameDesc': 'Randomly generate a common English surname',
'ms.paramsInput.englishFullName': 'English name',
'ms.paramsInput.englishFullNameDesc': 'Randomly generate a common English name',
'ms.paramsInput.chineseName': 'Chinese name',
'ms.paramsInput.chineseNameDesc': 'Randomly generate a common Chinese name',
'ms.paramsInput.chineseSurname': 'Chinese surname',
'ms.paramsInput.chineseSurnameDesc': 'Randomly generate a common Chinese surname',
'ms.paramsInput.chineseFullName': 'Full Chinese name',
'ms.paramsInput.chineseFullNameDesc': 'Randomly generate a common Chinese name',
'ms.paramsInput.color': 'color',
'ms.paramsInput.colorDesc': "Randomly generate colors in the format '#RRGGBB'",
'ms.paramsInput.RGBDesc': "Randomly generate colors in the format 'rgb(r, g, b)'",
'ms.paramsInput.RGBADesc': "Randomly generate colors in the format 'rgba(r, g, b, a)'",
'ms.paramsInput.hslDesc': "Randomly generate colors in the format 'hsl(h, s, l)'",
'ms.paramsInput.englishText': 'Large text',
'ms.paramsInput.englishTextDesc': 'Randomly generate a piece of text',
'ms.paramsInput.englishSentence': 'Sentence',
'ms.paramsInput.englishSentenceDesc':
'Randomly generate a sentence with the first letter of the first word capitalized',
'ms.paramsInput.englishWord': 'Word',
'ms.paramsInput.englishWordDesc': 'Generate a word randomly',
'ms.paramsInput.englishTitle': 'Title',
'ms.paramsInput.englishTitleDesc': 'Randomly generate a title',
'ms.paramsInput.chineseText': 'Large text',
'ms.paramsInput.chineseTextDesc': 'Randomly generate a Chinese text',
'ms.paramsInput.chineseSentence': 'Sentence',
'ms.paramsInput.chineseSentenceDesc': 'Randomly generate a Chinese sentence',
'ms.paramsInput.chineseWord': 'Single word',
'ms.paramsInput.chineseWordDesc': 'Randomly generate a Chinese character',
'ms.paramsInput.chineseTitle': 'Title',
'ms.paramsInput.chineseTitleDesc': 'Randomly generate a Chinese title',
'ms.paramsInput.regexp': 'Regular expressions',
'ms.paramsInput.regexpDesc': 'Return results based on regular expression',
'ms.paramsInput.addFunc': 'Add function',
'ms.paramsInput.md5Desc': 'MD5 encrypt',
'ms.paramsInput.base64Desc': 'Base64 encryption',
'ms.paramsInput.unbase64Desc': 'Base64 decryption',
'ms.paramsInput.substrDesc': 'Starting and ending',
'ms.paramsInput.substrStartPlaceholder': 'Starting value',
'ms.paramsInput.substrEndPlaceholder': 'Ending value',
'ms.paramsInput.concatconcat': 'String to end with',
'ms.paramsInput.concatconcatDesc': 'Ending string',
'ms.paramsInput.lconcatDesc': 'Starting string',
'ms.paramsInput.lconcat': 'The string to start with',
'ms.paramsInput.sha1Desc': 'SHA1 encryption',
'ms.paramsInput.sha224Desc': 'SHA224 encryption',
'ms.paramsInput.sha256Desc': 'SHA256 encryption',
'ms.paramsInput.sha384Desc': 'SHA384 encryption',
'ms.paramsInput.sha512Desc': 'SHA512 encryption',
'ms.paramsInput.lowerDesc': 'Convert all letters to lowercase',
'ms.paramsInput.upperDesc': 'Convert all letters to uppercase',
'ms.paramsInput.lengthDesc': 'Data length',
'ms.paramsInput.numberDesc': 'Convert string to number',
'ms.paramsInput.paramSetting': 'Parameter settings',
'ms.paramsInput.mockType': 'Mock type',
'ms.paramsInput.jmeterType': 'JMeter type',
'ms.paramsInput.mockTypePlaceholder': 'Please select parameter type',
'ms.paramsInput.personal': 'Personal information',
'ms.paramsInput.dateOrTime': 'Date/Time',
'ms.paramsInput.cText': 'Chinese text',
'ms.paramsInput.eText': 'English text',
'ms.paramsInput.web': 'Web variables',
'ms.paramsInput.number': 'Number',
'ms.paramsInput.base': 'Basic variables',
'ms.paramsInput.apply': 'Apply',
'ms.paramsInput.variable': 'Variable',
'ms.paramsInput.randomFromMultipleVars': 'Extract elements from a set of values of variables separated by |',
'ms.paramsInput.split': 'Split string into variables',
'ms.paramsInput.eval': 'Compute variable expression',
'ms.paramsInput.evalVar': 'Evaluate an expression stored in a variable',
'ms.paramsInput.V': 'Evaluate variable name',
'ms.paramsInput.code': 'Encode',
'ms.paramsInput.escapeHtml': 'Encode a string using HTML encoding',
'ms.paramsInput.escapeXml': 'Encode a string using XML encoding',
'ms.paramsInput.unescape': 'Handle strings containing Java escape characters (such as \\n and \\t)',
'ms.paramsInput.unescapeHtml': 'Decode HTML encoded string',
'ms.paramsInput.urldecode': 'Decode application/x-www-form-urlencoded string',
'ms.paramsInput.urlencode': 'Encode string to application/x-www-form-urlencoded string',
'ms.paramsInput.script': 'Script',
'ms.paramsInput.groovy': 'Run the Apache Groovy script',
'ms.paramsInput.BeanShell': 'Run BeanShell script',
'ms.paramsInput.javaScript': 'Processing JavaScript (Nashorn)',
'ms.paramsInput.jexl2': 'Evaluate Commons Jexl2 expressions',
'ms.paramsInput.jexl3': 'Evaluate Commons Jexl3 expressions',
'ms.paramsInput.jmeterTime': 'Returns the current time in various formats',
'ms.paramsInput.timeShift':
'Returns a date in various formats with a specified amount of seconds/minutes/hours/days added',
'ms.paramsInput.dateTimeConvert': 'Convert date or time from source format to target format',
'ms.paramsInput.RandomDate': 'Generate random dates within a specific date range',
'ms.paramsInput.property': 'Property',
'ms.paramsInput.isPropDefined': 'Test whether the attribute exists',
'ms.paramsInput.readProperty': 'Read properties',
'ms.paramsInput.P': 'Read attributes (shorthand method)',
'ms.paramsInput.setProperty': 'Set JMeter properties',
'ms.paramsInput.isVarDefined': 'Test whether the variable exists',
'ms.paramsInput.counter': 'Generate an increasing number',
'ms.paramsInput.intSum': 'Add an int number',
'ms.paramsInput.longSum': 'Add long numbers',
'ms.paramsInput.Random': 'Generate a random number',
'ms.paramsInput.StringFromFile': 'Read a line from a file',
'ms.paramsInput.file': 'File',
'ms.paramsInput.FileToString': 'Read the entire file',
'ms.paramsInput.CSVRead': 'Read from CSV delimited file',
'ms.paramsInput.XPath': 'Read from a file using XPath expressions',
'ms.paramsInput.StringToFile': 'Write string to file',
'ms.paramsInput.info': 'Info',
'ms.paramsInput.digest': 'Generate digest (SHA-1, SHA-256, MD5...)',
'ms.paramsInput.threadNum': 'Get thread group number',
'ms.paramsInput.threadGroupName': 'Get thread group name',
'ms.paramsInput.samplerName': 'Get sampler name (label)',
'ms.paramsInput.machineIP': 'Get the local IP address',
'ms.paramsInput.machineName': 'Get local machine name',
'ms.paramsInput.TestPlanName': 'Returns the name of the current test plan',
'ms.paramsInput.log': 'Log (or display) a message (and return a value)',
'ms.paramsInput.logn': 'Log (or display) a message (empty return value)',
'ms.paramsInput.RandomString': 'Generate a random string',
'ms.paramsInput.UUID': 'Generate random type 4 UUID',
'ms.paramsInput.char': 'Generate Unicode character values from a list of numbers',
'ms.paramsInput.changeCase': 'Change case according to different modes',
'ms.paramsInput.regexFunction': 'Parse previous responses using regular expressions',
};

View File

@ -0,0 +1,210 @@
export default {
'ms.paramsInput.value': '参数值',
'ms.paramsInput.placeholder': '以{at}开始,双击可快速输入',
'ms.paramsInput.preview': '参数预览',
'ms.paramsInput.natural': '自然数',
'ms.paramsInput.naturalDesc': '返回一个随机的自然数',
'ms.paramsInput.naturalRange': '1-100自然数',
'ms.paramsInput.naturalRangeDesc': '返回一个随机的1-100的自然数大于等于 1 的整数)',
'ms.paramsInput.max': '最大值',
'ms.paramsInput.maxNaturalPlaceholder': '输入 <=100 的整数,如 100',
'ms.paramsInput.min': '最小值',
'ms.paramsInput.minNaturalPlaceholder': '输入 >=1的整数如 1',
'ms.paramsInput.integer': '整数',
'ms.paramsInput.integerDesc': '返回一个随机的整数',
'ms.paramsInput.integerRange': '1-100整数',
'ms.paramsInput.integerRangeDesc': '返回随机的1-100的整数',
'ms.paramsInput.maxIntegerPlaceholder': '输入整数,如 100最大不超过 100',
'ms.paramsInput.minIntegerPlaceholder': '输入整数,如 1',
'ms.paramsInput.float': '浮点数',
'ms.paramsInput.floatDesc': '返回一个随机的浮点数整数1-10小数部分位数的最小值2最大值5',
'ms.paramsInput.floatMin': '小数部分最小值',
'ms.paramsInput.floatMax': '小数部分最大值',
'ms.paramsInput.floatIntegerMin': '整数部分最小值',
'ms.paramsInput.floatIntegerMax': '整数部分最大值',
'ms.paramsInput.commonPlaceholder': '请输入',
'ms.paramsInput.commonSelectPlaceholder': '请选择',
'ms.paramsInput.bool': '布尔值',
'ms.paramsInput.boolDesc': '随机生成一个布尔值',
'ms.paramsInput.character': '字符池',
'ms.paramsInput.characterDesc': '从字符串池返回随机的字符',
'ms.paramsInput.characterLower': '随机小写字符',
'ms.paramsInput.characterLowerDesc': '返回一个随机的小写字符',
'ms.paramsInput.characterUpper': '随机大写字符',
'ms.paramsInput.characterUpperDesc': '返回一个随机的大写字符',
'ms.paramsInput.characterSymbol': '特殊符号',
'ms.paramsInput.characterSymbolDesc': '返回一个随机的特殊符号',
'ms.paramsInput.characterFormat': '格式',
'ms.paramsInput.string': '字符串',
'ms.paramsInput.stringDesc': '从字符串池返回一个随机字符串字符数1-10',
'ms.paramsInput.stringMin': '最小字符数',
'ms.paramsInput.stringMax': '最大字符数',
'ms.paramsInput.integerArray': '整型数组',
'ms.paramsInput.integerArrayDesc': '返回一个整型数组参数分别start起始值stop结束值step步长',
'ms.paramsInput.integerArrayStartPlaceholder': '起始值',
'ms.paramsInput.integerArrayEndPlaceholder': '结束值',
'ms.paramsInput.integerArrayStepPlaceholder': '步长',
'ms.paramsInput.date': '日期',
'ms.paramsInput.dateDesc': '返回一个随机的日期字符串。例1983-01-29',
'ms.paramsInput.time': '时间',
'ms.paramsInput.timeDesc': '返回一个随机的时间字符串。 例20:47:37',
'ms.paramsInput.dateTime': '日期时间',
'ms.paramsInput.dateTimeDesc': '返回一个随机的日期和时间字符串。例1977-11-17 03:50:15',
'ms.paramsInput.nowDateTime': '当前日期时间',
'ms.paramsInput.nowDateTimeDesc': '返回当前日期字符串。例2014-04-29 20:08:38',
'ms.paramsInput.uuidDesc': '随机生成一个 UUID。例eFD616Bd-e149-c98E-a041-5e12ED0C94Fd',
'ms.paramsInput.url': '网址',
'ms.paramsInput.urlDesc': '随机生成一个http URL',
'ms.paramsInput.urlPlaceholder': '请输入内容',
'ms.paramsInput.protocol': '协议',
'ms.paramsInput.protocolDesc': '随机生成一个 URL 协议。例http ftp',
'ms.paramsInput.domain': '域名',
'ms.paramsInput.domainDesc': '随机生成一个域名',
'ms.paramsInput.topDomain': '顶级域名',
'ms.paramsInput.topDomainDesc': '随机生成一个顶级域名。例net',
'ms.paramsInput.email': '邮件地址',
'ms.paramsInput.emailDesc': '随机生成一个邮件地址',
'ms.paramsInput.ip': 'IP地址',
'ms.paramsInput.ipDesc': '随机生成一个IP地址',
'ms.paramsInput.location': '区域',
'ms.paramsInput.locationDesc': '随机生成一个(中国)大区。例:华北',
'ms.paramsInput.province': '省份',
'ms.paramsInput.provinceDesc': '随机生成一个(中国)省(或直辖市、自治区、特别行政区)',
'ms.paramsInput.city': '城市',
'ms.paramsInput.cityDesc': '随机生成一个(中国)市',
'ms.paramsInput.county': '地区',
'ms.paramsInput.countyDesc': '随机生成一个(中国)县',
'ms.paramsInput.provinceCityCounty': '省份/市/县',
'ms.paramsInput.provinceCityCountyDesc': '随机生成一个(中国)县(带省市)。例:甘肃省 白银市 会宁县',
'ms.paramsInput.zip': '邮编',
'ms.paramsInput.zipDesc': '随机生成一个邮政编码',
'ms.paramsInput.idCard': '身份证号',
'ms.paramsInput.idCardDesc': '随机生成一个身份证号(生成的身份证号码并不一定是真实有效的)',
'ms.paramsInput.specifyIdCard': '指定月份身份证号',
'ms.paramsInput.specifyIdCardDesc': '随机生成一个身份证号(可以指定生日)(生成的身份证号码并不一定是真实有效的)',
'ms.paramsInput.birthday': '出生日期',
'ms.paramsInput.birthdayPlaceholder': '请选择日期',
'ms.paramsInput.phone': '手机号',
'ms.paramsInput.phoneDesc': '随机生成一个手机号(生成的手机号码并不一定是真实有效的)',
'ms.paramsInput.englishName': '英文名',
'ms.paramsInput.englishNameDesc': '随机生成一个常见的英文名',
'ms.paramsInput.englishSurname': '英文姓',
'ms.paramsInput.englishSurnameDesc': '随机生成一个常见的英文姓',
'ms.paramsInput.englishFullName': '英文姓名',
'ms.paramsInput.englishFullNameDesc': '随机生成一个常见的英文姓名',
'ms.paramsInput.chineseName': '中文名',
'ms.paramsInput.chineseNameDesc': '随机生成一个常见的中文名',
'ms.paramsInput.chineseSurname': '中文姓',
'ms.paramsInput.chineseSurnameDesc': '随机生成一个常见的中文姓',
'ms.paramsInput.chineseFullName': '中文姓名',
'ms.paramsInput.chineseFullNameDesc': '随机生成一个常见的中文姓名',
'ms.paramsInput.color': '颜色',
'ms.paramsInput.colorDesc': "随机生成颜色,格式为 '#RRGGBB'",
'ms.paramsInput.RGBDesc': "随机生成颜色,格式为 'rgb(r, g, b)'",
'ms.paramsInput.RGBADesc': "随机生成颜色,格式为 'rgba(r, g, b, a)'",
'ms.paramsInput.hslDesc': "随机生成颜色,格式为 'hsl(h, s, l)'",
'ms.paramsInput.englishText': '大段文本',
'ms.paramsInput.englishTextDesc': '随机生成一段文本',
'ms.paramsInput.englishSentence': '句子',
'ms.paramsInput.englishSentenceDesc': '随机生成一个句子,第一个单词的首字母大写',
'ms.paramsInput.englishWord': '单词',
'ms.paramsInput.englishWordDesc': '随机生成一个单词',
'ms.paramsInput.englishTitle': '标题',
'ms.paramsInput.englishTitleDesc': '随机生成一个标题',
'ms.paramsInput.chineseText': '大段文本',
'ms.paramsInput.chineseTextDesc': '随机生成一段中文文本',
'ms.paramsInput.chineseSentence': '句子',
'ms.paramsInput.chineseSentenceDesc': '随机生成一个中文句子',
'ms.paramsInput.chineseWord': '单字',
'ms.paramsInput.chineseWordDesc': '随机生成一个汉字',
'ms.paramsInput.chineseTitle': '标题',
'ms.paramsInput.chineseTitleDesc': '随机生成一个中文标题',
'ms.paramsInput.regexp': '正则表达式',
'ms.paramsInput.regexpDesc': '根据正则表达式返回结果',
'ms.paramsInput.addFunc': '添加函数',
'ms.paramsInput.md5Desc': 'md5 加密',
'ms.paramsInput.base64Desc': 'base64 加密',
'ms.paramsInput.unbase64Desc': 'base64 解密',
'ms.paramsInput.substrDesc': '起止',
'ms.paramsInput.substrStartPlaceholder': '起始值',
'ms.paramsInput.substrEndPlaceholder': '结束值',
'ms.paramsInput.concatconcat': '要作为结尾的字符串',
'ms.paramsInput.concatconcatDesc': '结尾字符串',
'ms.paramsInput.lconcatDesc': '开头字符串',
'ms.paramsInput.lconcat': '要作为开头的字符串',
'ms.paramsInput.sha1Desc': 'sha1 加密',
'ms.paramsInput.sha224Desc': 'sha224 加密',
'ms.paramsInput.sha256Desc': 'sha256 加密',
'ms.paramsInput.sha384Desc': 'sha384 加密',
'ms.paramsInput.sha512Desc': 'sha512 加密',
'ms.paramsInput.lowerDesc': '所有字母转为小写',
'ms.paramsInput.upperDesc': '所有字母转为大写',
'ms.paramsInput.lengthDesc': '数据长度',
'ms.paramsInput.numberDesc': '字符串转数字',
'ms.paramsInput.paramSetting': '参数设置',
'ms.paramsInput.mockType': 'mock类型',
'ms.paramsInput.jmeterType': 'JMeter类型',
'ms.paramsInput.mockTypePlaceholder': '请选择参数类型',
'ms.paramsInput.personal': '个人信息',
'ms.paramsInput.dateOrTime': '日期/时间',
'ms.paramsInput.cText': '中文文本',
'ms.paramsInput.eText': '英文文本',
'ms.paramsInput.web': 'web变量',
'ms.paramsInput.number': '数字',
'ms.paramsInput.base': '基础变量',
'ms.paramsInput.apply': '应用',
'ms.paramsInput.variable': '变量',
'ms.paramsInput.randomFromMultipleVars': '从由|分隔的一组变量的值中提取元素',
'ms.paramsInput.split': '将字符串拆分为变量',
'ms.paramsInput.eval': '计算变量表达式',
'ms.paramsInput.evalVar': '计算存储在变量中的表达式',
'ms.paramsInput.V': '评估变量名',
'ms.paramsInput.code': '编码',
'ms.paramsInput.escapeHtml': '使用 HTML 编码对字符串进行编码',
'ms.paramsInput.escapeXml': '使用 XML 编码对字符串进行编码',
'ms.paramsInput.unescape': '处理包含 Java 转义符的字符串(例如 \\n 和 \\t',
'ms.paramsInput.unescapeHtml': '解码 HTML 编码的字符串',
'ms.paramsInput.urldecode': '解码 application/x-www-form-urlencoded 字符串',
'ms.paramsInput.urlencode': '将字符串编码为 application/x-www-form-urlencoded 字符串',
'ms.paramsInput.script': '脚本',
'ms.paramsInput.groovy': '运行 Apache Groovy 脚本',
'ms.paramsInput.BeanShell': '运行 BeanShell 脚本',
'ms.paramsInput.javaScript': '处理 JavaScript (Nashorn)',
'ms.paramsInput.jexl2': '评估 Commons Jexl2 表达式',
'ms.paramsInput.jexl3': '评估 Commons Jexl3 表达式',
'ms.paramsInput.jmeterTime': '以各种格式返回当前时间',
'ms.paramsInput.timeShift': '返回各种格式的日期,并添加指定的秒/分钟/小时/天量',
'ms.paramsInput.dateTimeConvert': '将日期或时间从源格式转换为目标格式',
'ms.paramsInput.RandomDate': '生成特定日期范围内的随机日期',
'ms.paramsInput.property': '属性',
'ms.paramsInput.isPropDefined': '测试属性是否存在',
'ms.paramsInput.readProperty': '读取属性',
'ms.paramsInput.P': '读取属性(简写方法)',
'ms.paramsInput.setProperty': '设置 JMeter 属性',
'ms.paramsInput.isVarDefined': '测试变量是否存在',
'ms.paramsInput.counter': '生成一个递增的数字',
'ms.paramsInput.intSum': '添加 int 数字',
'ms.paramsInput.longSum': '添加长数字',
'ms.paramsInput.Random': '生成一个随机数',
'ms.paramsInput.StringFromFile': '从文件中读取一行',
'ms.paramsInput.file': '文件',
'ms.paramsInput.FileToString': '读取整个文件',
'ms.paramsInput.CSVRead': '从 CSV 分隔文件中读取',
'ms.paramsInput.XPath': '使用 XPath 表达式从文件中读取',
'ms.paramsInput.StringToFile': '将字符串写入文件',
'ms.paramsInput.info': '信息',
'ms.paramsInput.digest': '生成摘要SHA-1、SHA-256、MD5...',
'ms.paramsInput.threadNum': '获取线程组号',
'ms.paramsInput.threadGroupName': '获取线程组名称',
'ms.paramsInput.samplerName': '获取采样器名称(标签)',
'ms.paramsInput.machineIP': '获取本机IP地址',
'ms.paramsInput.machineName': '获取本地机器名',
'ms.paramsInput.TestPlanName': '返回当前测试计划的名称',
'ms.paramsInput.log': '记录(或显示)一条消息(并返回值)',
'ms.paramsInput.logn': '记录(或显示)一条消息(空返回值)',
'ms.paramsInput.RandomString': '生成一个随机字符串',
'ms.paramsInput.UUID': '生成随机类型 4 UUID',
'ms.paramsInput.char': '从数字列表生成 Unicode 字符值',
'ms.paramsInput.changeCase': '根据不同模式更改大小写',
'ms.paramsInput.regexFunction': '使用正则表达式解析先前的响应',
};

View File

@ -0,0 +1,95 @@
<template>
<a-form-item v-for="(group, index) of props.inputGroup" :key="index" :label="t(group.label || '')" class="mb-[16px]">
<a-select
v-if="group.type === 'select'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:options="group.options"
:placeholder="t(group.placeholder || '')"
/>
<a-input
v-else-if="group.type === 'input'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:placeholder="t(group.placeholder || '')"
/>
<a-radio-group
v-else-if="group.type === 'radio'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
type="button"
>
<a-radio v-for="(option, i) of group.options || []" :key="`option${i}`" :value="option.value">
{{ t(option.label) }}
</a-radio>
</a-radio-group>
<a-date-picker
v-else-if="group.type === 'date'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:placeholder="t(group.placeholder || '')"
/>
<a-input-number
v-else-if="group.type === 'number'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:placeholder="t(group.placeholder || '')"
/>
<a-input
v-else-if="group.type === 'inputAppendSelect'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:placeholder="t(group.placeholder || '')"
class="ms-params-input-inputAppendSelect"
>
<template #prepend>
<a-select
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:options="group.options"
class="select-input-prepend !w-[70px]"
/>
</template>
</a-input>
</a-form-item>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
import type { MockParamInputGroupItem } from './types';
const props = withDefaults(
defineProps<{
paramForm: Record<string, any>;
type?: 'var' | 'func';
inputGroup: MockParamInputGroupItem[];
}>(),
{
type: 'var',
}
);
const emit = defineEmits<{
(e: 'update:paramForm', value: Record<string, any>): void;
}>();
const { t } = useI18n();
const innerForm = useVModel(props, 'paramForm', emit);
const paramKey = computed(() => {
return props.type === 'var' ? 'param' : 'funcParam';
});
watch(
() => props.inputGroup,
(arr) => {
arr.forEach((e, i) => {
if (innerForm.value[`${paramKey.value}${i + 1}`] === '') {
innerForm.value[`${paramKey.value}${i + 1}`] = e.value;
}
});
},
{
immediate: true,
}
);
</script>
<style lang="less" scoped>
.ms-input-group--prepend();
</style>

View File

@ -0,0 +1,24 @@
// mock 参数配置项输入组选项
export interface MockParamInputGroupOptionItem {
label: string;
value: string;
}
// mock 参数配置项输入组项类型
export type MockParamInputGroupItemType = 'select' | 'input' | 'number' | 'date' | 'radio' | 'inputAppendSelect';
// mock 参数配置项输入组
export interface MockParamInputGroupItem {
type: MockParamInputGroupItemType;
label?: string;
value?: string | number;
placeholder?: string;
options?: MockParamInputGroupOptionItem[];
inputValue?: string;
selectValue?: string;
}
// mock 参数配置项
export interface MockParamItem {
label: string;
value: string;
desc?: string;
inputGroup?: MockParamInputGroupItem[];
}

View File

@ -374,7 +374,7 @@
<style lang="less">
.ms-tree-container {
.ms-container--shadow();
.ms-container--shadow-y();
.ms-tree {
.ms-scroll-bar();
.arco-tree-node {
@ -486,5 +486,8 @@
}
}
}
.arco-tree-node-disabled-selectable {
@apply !cursor-default;
}
}
</style>

View File

@ -1,9 +1,10 @@
<template>
<div
:class="`ms-button ms-button-${props.type} ms-button--${props.status} ${
props.disabled || props.loading ? `ms-button--${props.status}--disabled` : ''
props.disabled || props.loading ? `ms-button--disabled ms-button--${props.status}--disabled` : ''
}`"
@click="clickHandler"
:disabled="props.disabled"
@click.stop="clickHandler"
>
<slot></slot>
<icon-loading v-if="props.loading" />
@ -92,4 +93,10 @@
background-color: rgb(var(--danger-1));
}
}
.ms-button--disabled {
@apply cursor-not-allowed;
}
.ms-button--secondary--disabled {
color: var(--color-text-brand);
}
</style>

View File

@ -4,7 +4,7 @@
<slot name="title">
<span class="font-medium">{{ title }}</span>
</slot>
<div class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]" @click="toggle">
<div v-if="showFullScreen" class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]" @click="toggle">
<MsIcon v-if="isFullscreen" type="icon-icon_minify_outlined" />
<MsIcon v-else type="icon-icon_magnify_outlined" />
{{ t('msCodeEditor.fullScreen') }}

View File

@ -83,4 +83,8 @@ export const editorProps = {
title: {
type: String as PropType<string>,
},
showFullScreen: {
type: Boolean as PropType<boolean>,
default: true,
},
};

View File

@ -28,7 +28,7 @@
</div>
</slot>
</template>
<div class="handle" @mousedown="startResize">
<div v-if="!props.disabledWidthDrag" class="handle" @mousedown="startResize">
<icon-drag-dot-vertical class="absolute left-[-3px] top-[50%] w-[14px]" size="14" />
</div>
<a-scrollbar class="h-full overflow-y-auto">
@ -73,7 +73,7 @@
</template>
<script setup lang="ts">
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import { defineAsyncComponent, ref, watch } from 'vue';
import type { Description } from '@/components/pure/ms-description/index.vue';
@ -100,6 +100,7 @@
width: number;
noContentPadding?: boolean; //
popupContainer?: string;
disabledWidthDrag?: boolean; //
}
const props = withDefaults(defineProps<DrawerProps>(), {
@ -108,6 +109,7 @@
showSkeleton: false,
showContinue: false,
popupContainer: 'body',
disabledWidthDrag: false,
});
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue']);

View File

@ -0,0 +1,196 @@
<template>
<div class="tab-container">
<a-tooltip v-if="!isNotOverflow" :content="t('ms.editableTab.arrivedLeft')" :disabled="!arrivedState.left">
<MsButton
type="icon"
status="secondary"
class="tab-button !mr-[4px]"
:disabled="arrivedState.left"
@click="scrollTabs('left')"
>
<MsIcon type="icon-icon_left_outlined" />
</MsButton>
</a-tooltip>
<div ref="tabNav" class="tab-nav">
<div
v-for="tab in props.tabs"
:key="tab.id"
class="tab"
:class="{ active: innerActiveTab === tab.id }"
@click="handleTabClick(tab)"
>
<div class="flex items-center">
<slot name="label" :tab="tab">{{ tab.label }}</slot>
<MsButton
v-if="tab.closable"
type="icon"
status="secondary"
class="tab-close-button"
@click="() => close(tab)"
>
<MsIcon type="icon-icon_close_outlined" size="12" />
</MsButton>
</div>
</div>
</div>
<a-tooltip v-if="!isNotOverflow" :content="t('ms.editableTab.arrivedRight')" :disabled="!arrivedState.right">
<MsButton
type="icon"
status="secondary"
class="tab-button !mr-[8px]"
:disabled="arrivedState.right"
@click="scrollTabs('right')"
>
<MsIcon type="icon-icon_right_outlined" />
</MsButton>
</a-tooltip>
<MsButton type="icon" status="secondary" class="tab-button !mr-[4px]" @click="addTab">
<MsIcon type="icon-icon_add_outlined" />
</MsButton>
<MsMoreAction v-if="props.moreActionList" :list="props.moreActionList">
<MsButton type="icon" status="secondary" class="tab-button">
<MsIcon type="icon-icon_more_outlined" />
</MsButton>
</MsMoreAction>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue';
import { useScroll, useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { useI18n } from '@/hooks/useI18n';
import type { TabItem } from './types';
const props = defineProps<{
tabs: TabItem[];
activeTab: string | number;
moreActionList?: ActionsItem[];
}>();
const emit = defineEmits<{
(e: 'update:activeTab', activeTab: string | number): void;
(e: 'add'): void;
(e: 'close', item: TabItem): void;
(e: 'click', item: TabItem): void;
}>();
const { t } = useI18n();
const innerActiveTab = useVModel(props, 'activeTab', emit);
const tabNav = ref<HTMLElement | null>(null);
const { arrivedState } = useScroll(tabNav);
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); //
const scrollTabs = (direction: 'left' | 'right') => {
if (tabNav.value) {
const tabNavWidth = tabNav.value?.clientWidth || 0;
const tabNavScrollWidth = tabNav.value?.scrollWidth || 0;
if (direction === 'left') {
tabNav.value.scrollTo({
left: tabNav.value.scrollLeft - tabNavWidth - 80,
behavior: 'smooth',
});
} else if (tabNavScrollWidth > tabNav.value.scrollLeft + tabNavWidth - 80) {
tabNav.value.scrollTo({
left: tabNav.value.scrollLeft + tabNavWidth,
behavior: 'smooth',
});
}
}
};
const scrollToActiveTab = () => {
const activeTabDom = tabNav.value?.querySelector('.tab.active');
if (activeTabDom) {
const tabRect = activeTabDom.getBoundingClientRect();
const navRect = tabNav.value?.getBoundingClientRect();
if (tabRect.left < navRect!.left) {
scrollTabs('left');
} else if (tabRect.right > navRect!.right) {
scrollTabs('right');
}
}
};
watch(props.tabs, () => {
nextTick(() => {
scrollToActiveTab();
});
});
onMounted(() => {
const resizeObserver = new ResizeObserver(() => {
scrollToActiveTab();
});
resizeObserver.observe(tabNav.value as Element);
});
function addTab() {
emit('add');
}
function close(item: TabItem) {
emit('close', item);
}
function handleTabClick(item: TabItem) {
innerActiveTab.value = item.id;
nextTick(() => {
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
</script>
<style lang="less" scoped>
.tab-container {
@apply flex items-center;
height: 32px;
.tab-nav {
@apply relative flex overflow-x-auto whitespace-nowrap;
&::-webkit-scrollbar {
width: 0; /* 宽度为0隐藏垂直滚动条 */
height: 0; /* 高度为0隐藏水平滚动条 */
}
.tab {
@apply flex cursor-pointer items-center;
margin-right: 4px;
padding: 5px 8px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
gap: 8px;
&.active,
&:hover {
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-1));
.tab-close-button {
@apply visible;
}
}
.tab-close-button {
@apply invisible !rounded-full;
margin-left: 4px !important;
}
}
}
.tab-button {
padding: 8px;
&:not([disabled='true']) {
padding: 8px;
color: var(--color-text-4);
&:hover {
color: var(--color-text-1);
}
}
}
}
</style>

View File

@ -0,0 +1,4 @@
export default {
'ms.editableTab.arrivedLeft': 'Already reached the far left~',
'ms.editableTab.arrivedRight': 'Already reached the far right~',
};

View File

@ -0,0 +1,4 @@
export default {
'ms.editableTab.arrivedLeft': '到最左侧啦~',
'ms.editableTab.arrivedRight': '到最右侧啦~',
};

View File

@ -0,0 +1,6 @@
export interface TabItem {
id: string | number;
label: string;
closable?: boolean;
[key: string]: any;
}

View File

@ -169,7 +169,7 @@
<style lang="less" scoped>
.ms-list {
.ms-container--shadow();
.ms-container--shadow-y();
:deep(.arco-list) {
@apply rounded-none;
.ms-list-item {

View File

@ -3,13 +3,26 @@
v-model:size="innerSize"
:min="props.min"
:max="props.max"
:class="['h-full', isExpanded ? '' : 'expanded-panel', isExpandAnimating ? 'animating' : '']"
:class="[
'h-full',
isExpanded ? '' : 'expanded-panel',
isExpandAnimating ? 'animating' : '',
props.direction === 'vertical' ? 'ms-split-box--vertical' : '',
]"
:direction="props.direction"
:disabled="props.disabled || !isExpanded"
>
<template #first>
<div v-if="props.direction === 'horizontal'" class="ms-split-box ms-split-box--left">
<div v-if="props.expandDirection === 'right'" class="absolute right-0 flex h-full w-[16px] items-center">
<div class="expand-icon expand-icon--left" @click="changeLeftExpand">
<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)]' : ''
}`"
>
<div
v-if="props.direction === 'horizontal' && props.expandDirection === 'right' && !props.disabled"
class="absolute right-0 h-full w-[16px]"
>
<div class="expand-icon expand-icon--left" @click="() => changeExpand()">
<MsIcon
:type="isExpanded ? 'icon-icon_up-left_outlined' : 'icon-icon_down-right_outlined'"
class="text-[var(--color-text-brand)]"
@ -17,41 +30,30 @@
/>
</div>
</div>
<slot name="left"></slot>
<slot name="first"></slot>
</div>
<div v-else class="ms-split-box ms-split-box--top">
<slot name="top"></slot>
</template>
<template #resize-trigger>
<div :class="props.direction === 'horizontal' ? 'horizontal-expand-line' : 'vertical-expand-line'">
<div v-if="isExpanded" class="expand-color-line"></div>
</div>
</template>
<template #second>
<template v-if="props.direction === 'horizontal'">
<div v-if="props.expandDirection === 'left'" class="absolute flex h-full w-[16px] items-center">
<div class="expand-icon" @click="changeLeftExpand">
<MsIcon
:type="isExpanded ? 'icon-icon_up-left_outlined' : 'icon-icon_down-right_outlined'"
class="text-[var(--color-text-brand)]"
size="12"
/>
</div>
<div
v-if="props.direction === 'horizontal' && props.expandDirection === 'left' && !props.disabled"
class="absolute h-full w-[16px]"
>
<div class="expand-icon" @click="() => changeExpand()">
<MsIcon
:type="isExpanded ? 'icon-icon_up-left_outlined' : 'icon-icon_down-right_outlined'"
class="text-[var(--color-text-brand)]"
size="12"
/>
</div>
<div class="ms-split-box ms-split-box--right">
<slot name="right"></slot>
</div>
</template>
<template v-else>
<div class="absolute top-0 flex h-[16px] w-full items-center justify-center">
<div class="expand-icon expand-icon--vertical" @click="changeLeftExpand">
<MsIcon
:type="isExpanded ? 'icon-icon_up-left_outlined' : 'icon-icon_down-right_outlined'"
class="text-[var(--color-text-brand)]"
size="12"
/>
</div>
</div>
<div class="ms-split-box ms-split-box--bottom">
<slot name="bottom"></slot>
</div>
</template>
</div>
<div :class="`ms-split-box ${props.direction === 'horizontal' ? 'ms-split-box--right' : 'ms-split-box--bottom'}`">
<slot name="second"></slot>
</div>
</template>
</a-split>
</template>
@ -68,6 +70,7 @@
max?: number | string;
direction?: 'horizontal' | 'vertical';
expandDirection?: 'left' | 'right' | 'top'; // TODO: bottom left top
disabled?: boolean; //
}>(),
{
size: '300px',
@ -81,6 +84,9 @@
const emit = defineEmits(['update:size', 'expandChange']);
const innerSize = ref(props.size || '300px');
const initialSize = props.size || '300px';
const isExpanded = ref(true);
const isExpandAnimating = ref(false); //
watch(
() => props.size,
@ -98,32 +104,45 @@
}
);
const isExpanded = ref(true);
const isExpandAnimating = ref(false); //
function changeLeftExpand() {
function expand(size?: string | number) {
isExpandAnimating.value = true;
isExpanded.value = !isExpanded.value;
if (isExpanded.value) {
innerSize.value = props.size || '300px'; // size /
emit('expandChange', true);
} else {
innerSize.value = props.expandDirection === 'right' ? 1 : '0px'; // expandDirection right 100%
emit('expandChange', false);
}
isExpanded.value = true;
innerSize.value = size || initialSize || '300px'; // size /
emit('expandChange', true);
//
setTimeout(() => {
isExpandAnimating.value = false;
}, 300);
}
function collapse(size?: string | number) {
isExpandAnimating.value = true;
isExpanded.value = false;
innerSize.value = props.expandDirection === 'right' ? 1 : size || '0px'; // expandDirection right 100%
emit('expandChange', false);
//
setTimeout(() => {
isExpandAnimating.value = false;
}, 300);
}
function changeExpand() {
if (isExpanded.value) {
collapse();
} else {
expand();
}
}
defineExpose({
expand,
collapse,
});
</script>
<style lang="less" scoped>
/* stylelint-disable value-keyword-case */
.expanded-panel {
:deep(.arco-split-trigger) {
@apply hidden;
}
:deep(.arco-split-pane) {
@apply relative overflow-hidden;
}
@ -146,8 +165,10 @@
width: calc(v-bind(innerSize) - 4px);
}
.expand-icon {
@apply z-10 flex cursor-pointer justify-center;
@apply relative z-20 flex cursor-pointer justify-center;
top: 25%;
transform: translateY(50%);
padding: 12px 2px;
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
background-color: var(--color-text-n8);
@ -158,15 +179,50 @@
:deep(.arco-split-trigger-icon) {
font-size: 14px;
}
.ms-split-box--bottom {
@apply h-full;
}
.expand-icon--vertical {
@apply rotate-90;
}
.expand-icon--left {
@apply rotate-180;
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
}
.horizontal-expand-line {
padding: 0 1px;
height: 100%;
.expand-color-line {
width: 1px;
height: 100%;
background-color: var(--color-text-n8);
}
&:hover,
&:active {
background-color: rgb(var(--primary-5));
.expand-color-line {
background-color: transparent;
}
}
}
.ms-split-box--vertical {
.ms-split-box--bottom {
@apply h-full bg-white;
}
.vertical-expand-line {
@apply relative z-10 flex items-center justify-center bg-transparent;
&::before {
@apply absolute w-full;
margin-bottom: -4px;
height: 4px;
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%), 0 -1px 4px 0 rgb(255 255 255), 0 -1px 4px 0 rgb(255 255 255),
0 -1px 4px 0 rgb(255 255 255);
content: '';
}
// .expand-icon--vertical {
// width: 20px;
// height: 0;
// margin-top: 4px;
// background-color: transparent;
// border-radius: 2px;
// background-color: var(--color-text-n8);
// }
}
}
</style>

View File

@ -357,7 +357,7 @@ export default function useTableProps<T>(
resetSelector();
} else {
resetSelector(false);
data.forEach((item) => {
data.forEach((item: Record<string, any>) => {
if (item[rowKey] && !selectedKeys.has(item[rowKey])) {
selectedKeys.add(item[rowKey]);
}
@ -386,22 +386,24 @@ export default function useTableProps<T>(
});
watchEffect(() => {
const { heightUsed, showPagination, selectedKeys, msPagination } = propsRes.value;
let hasFooterAction = false;
if (showPagination) {
const { pageSize, total } = msPagination as Pagination;
/*
*
* 1.
* 2.
*/
hasFooterAction = total > pageSize || selectedKeys.size > 0;
}
if (props?.heightUsed) {
const { heightUsed, showPagination, selectedKeys, msPagination } = propsRes.value;
let hasFooterAction = false;
if (showPagination) {
const { pageSize, total } = msPagination as Pagination;
/*
*
* 1.
* 2.
*/
hasFooterAction = total > pageSize || selectedKeys.size > 0;
}
const currentY =
appStore.innerHeight - (heightUsed || defaultHeightUsed) + (hasFooterAction ? 0 : footerActionWrapHeight);
propsRes.value.showFooterActionWrap = hasFooterAction;
propsRes.value.scroll = { ...propsRes.value.scroll, y: currentY };
const currentY =
appStore.innerHeight - (heightUsed || defaultHeightUsed) + (hasFooterAction ? 0 : footerActionWrapHeight);
propsRes.value.showFooterActionWrap = hasFooterAction;
propsRes.value.scroll = { ...propsRes.value.scroll, y: currentY };
}
});
return {

View File

@ -27,6 +27,23 @@ export interface PathMapItem {
* children /tab集合
*/
export const pathMap: PathMapItem[] = [
{
key: 'API_TEST', // 接口测试
locale: 'menu.apiTest',
route: RouteEnum.API_TEST,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'API_TEST_DEBUG', // 接口测试
locale: 'menu.apiTest.debug',
route: RouteEnum.API_TEST_DEBUG,
permission: [],
level: MENU_LEVEL[2],
children: [],
},
],
},
{
key: 'CASE_MANAGEMENT', // 功能测试
locale: 'menu.caseManagement',
@ -41,13 +58,6 @@ export const pathMap: PathMapItem[] = [
permission: [],
level: MENU_LEVEL[2],
},
// {
// key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例-创建用例
// locale: 'menu.caseManagement.featureCaseDetail',
// route: RouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
// permission: [],
// level: MENU_LEVEL[2],
// },
{
key: 'CASE_MANAGEMENT_REVIEW', // 功能测试-功能用例-用例评审
locale: 'menu.caseManagement.caseManagementReview',
@ -55,6 +65,27 @@ export const pathMap: PathMapItem[] = [
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例详情
locale: 'menu.caseManagement.featureCaseDetail',
route: RouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_CASE_CREATE_SUCCESS', // 功能测试-功能用例创建成功页面
locale: 'menu.caseManagement.featureCaseCreateSuccess',
route: RouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_CASE_RECYCLE', // 功能测试-功能用例-回收站
locale: 'menu.caseManagement.featureCaseRecycle',
route: RouteEnum.CASE_MANAGEMENT_CASE_RECYCLE,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_REVIEW_CREATE', // 功能测试-功能用例-创建评审
locale: 'menu.caseManagement.caseManagementReviewCreate',
@ -392,41 +423,4 @@ export const pathMap: PathMapItem[] = [
},
],
},
{
key: 'CASE_MANAGEMENT', // 功能测试
locale: 'menu.caseManagement',
route: RouteEnum.CASE_MANAGEMENT,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'CASE_MANAGEMENT_CASE', // 功能测试-功能用例
locale: 'menu.caseManagement.featureCase',
route: RouteEnum.CASE_MANAGEMENT_CASE,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例详情
locale: 'menu.caseManagement.featureCaseDetail',
route: RouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_CASE_CREATE_SUCCESS', // 功能测试-功能用例创建成功页面
locale: 'menu.caseManagement.featureCaseCreateSuccess',
route: RouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_CASE_RECYCLE', // 功能测试-功能用例-回收站
locale: 'menu.caseManagement.featureCaseRecycle',
route: RouteEnum.CASE_MANAGEMENT_CASE_RECYCLE,
permission: [],
level: MENU_LEVEL[2],
},
],
},
];

View File

@ -0,0 +1,22 @@
export enum RequestMethods {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
PATCH = 'PATCH',
OPTIONS = 'OPTIONS',
HEAD = 'HEAD',
CONNECT = 'CONNECT',
}
export enum RequestComposition {
HEADER = 'HEADER',
BODY = 'BODY',
QUERY = 'QUERY',
REST = 'REST',
PREFIX = 'PREFIX',
POST_CONDITION = 'POST_CONDITION',
ASSERTION = 'ASSERTION',
AUTH = 'AUTH',
SETTING = 'SETTING',
}

View File

@ -1,5 +1,6 @@
export enum ApiTestRouteEnum {
API_TEST = 'apiTest',
API_TEST_DEBUG = 'apiTestDebug',
}
export enum BugManagementRouteEnum {

View File

@ -16,7 +16,7 @@ export interface ContainerShadowOptions {
}
/**
* .ms-container--shadow() 使
* .ms-container--shadow-y() 使
* @param options ContainerShadowOptions
*/
export default function useContainerShadow(options: ContainerShadowOptions) {

View File

@ -64,10 +64,12 @@ export default {
'common.resetDefault': 'Reset default',
'common.tagPlaceholder': 'Add tag and press Enter to end',
'common.batchModify': 'Batch Edit',
'common.batchAdd': 'Batch Add',
'common.pleaseSelect': 'please choose',
'common.quickAddMember': 'Quickly add members',
'common.filter': 'Filter',
'common.export': 'Export',
'common.import': 'Import',
'common.collapseAll': 'Collapse all',
'common.expandAll': 'Expand all',
'common.copy': 'Copy',

View File

@ -24,6 +24,7 @@ export default {
'menu.bugManagement': 'Bug',
'menu.caseManagement': 'Case Management',
'menu.apiTest': 'API Test',
'menu.apiTest.debug': 'API debug',
'menu.uiTest': 'UI Test',
'menu.performanceTest': 'Performance Test',
'menu.projectManagement': 'Project',

View File

@ -67,9 +67,11 @@ export default {
'common.resetDefault': '恢复默认',
'common.tagPlaceholder': '添加标签回车结束',
'common.batchModify': '批量修改',
'common.batchAdd': '批量添加',
'common.pleaseSelect': '请选择',
'common.quickAddMember': '快速添加成员',
'common.export': '导出',
'common.import': '导入',
'common.collapseAll': '展开全部',
'common.expandAll': '收起全部',
'common.copy': '复制',

View File

@ -23,6 +23,7 @@ export default {
'menu.bugManagement': '缺陷管理',
'menu.caseManagement': '功能测试',
'menu.apiTest': '接口测试',
'menu.apiTest.debug': '接口调试',
'menu.uiTest': 'UI测试',
'menu.workstation': '工作台',
'menu.loadTest': '性能测试',

View File

@ -6,7 +6,7 @@ import type { AppRouteRecordRaw } from '../types';
const ApiTest: AppRouteRecordRaw = {
path: '/api-test',
name: ApiTestRouteEnum.API_TEST,
redirect: '/api-test/index',
redirect: '/api-test/debug',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.apiTest',
@ -16,11 +16,13 @@ const ApiTest: AppRouteRecordRaw = {
},
children: [
{
path: 'index',
name: 'apiTestIndex',
component: () => import('@/views/api-test/index.vue'),
path: 'debug',
name: ApiTestRouteEnum.API_TEST_DEBUG,
component: () => import('@/views/api-test/debug/index.vue'),
meta: {
locale: 'menu.apiTest.debug',
roles: ['*'],
isTopMenu: true,
},
},
],

View File

@ -33,3 +33,31 @@ export function removeEventListen(
target.removeEventListener(event, handler, capture);
}
}
/**
* Ctrl + S
* @param callback
*/
export function registerCatchSaveShortcut(callback: () => void) {
document.addEventListener('keydown', (event) => {
// 检查是否按下了 Ctrl 键Windows/Linux或 Command 键Mac
const isCtrlPressed = event.ctrlKey || event.metaKey;
// 检查是否按下了 'S' 键
const isSPressed = event.key === 's';
// 如果同时按下了 Ctrl 键和 'S' 键
if (isCtrlPressed && isSPressed) {
// 阻止默认行为,防止浏览器保存页面
event.preventDefault();
// 在这里添加你的保存逻辑
callback();
}
});
}
/**
* Ctrl + S
* @param callback
*/
export function removeCatchSaveShortcut(callback: () => void) {
document.removeEventListener('keydown', callback);
}

View File

@ -0,0 +1,45 @@
<template>
<div class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
</template>
<script setup lang="ts">
import { RequestMethods } from '@/enums/apiEnum';
const props = defineProps<{
method: RequestMethods;
}>();
const colorMaps = [
{
color: 'rgb(var(--success-6))',
includes: [RequestMethods.GET, RequestMethods.HEAD],
},
{
color: 'rgb(var(--warning-7))',
includes: [RequestMethods.POST],
},
{
color: 'rgb(var(--link-7))',
includes: [RequestMethods.PUT, RequestMethods.OPTIONS],
},
{
color: 'rgb(var(--danger-6))',
includes: [RequestMethods.DELETE],
},
{
color: 'rgb(var(--primary-7))',
includes: [RequestMethods.PATCH],
},
{
color: 'rgb(var(--primary-4))',
includes: [RequestMethods.CONNECT],
},
];
const methodColor = computed(() => {
const colorMap = colorMaps.find((item) => item.includes.includes(props.method));
return colorMap?.color;
});
</script>
<style lang="less" scoped></style>

View File

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

View File

@ -0,0 +1,262 @@
<template>
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
<MsEditableTab
v-model:active-tab="activeTab"
:tabs="debugTabs"
:more-action-list="moreActionList"
@add="addDebugTab"
@close="closeDebugTab"
@click="setActiveDebug"
>
<template #label="{ tab }">
<apiMethodName :method="tab.method" class="mr-[4px]" />
{{ tab.label }}
<div class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
</template>
</MsEditableTab>
</div>
<div class="px-[24px] pt-[16px]">
<div class="mb-[4px] flex items-center justify-between">
<a-input-group class="flex-1">
<a-select v-model:model-value="activeDebug.method" class="w-[140px]">
<template #label="{ data }">
<apiMethodName :method="data.value" class="inline-block" />
</template>
<a-option v-for="method of RequestMethods" :key="method" :value="method">
<apiMethodName :method="method" />
</a-option>
</a-select>
<a-input v-model:model-value="debugUrl" :placeholder="t('ms.apiTestDebug.urlPlaceholder')" />
</a-input-group>
<div class="ml-[16px]">
<a-dropdown-button class="exec-btn">
{{ t('ms.apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption>{{ t('ms.apiTestDebug.localExec') }}</a-doption>
</template>
</a-dropdown-button>
<a-button type="secondary">
<div class="flex items-center">
{{ t('common.save') }}
<div class="text-[var(--color-text-4)]">(<icon-command size="14" /> + S)</div>
</div>
</a-button>
</div>
</div>
</div>
<div ref="splitContainerRef" class="flex-1">
<MsSplitBox
ref="splitBoxRef"
v-model:size="splitBoxSize"
:max="0.98"
min="10px"
:direction="activeLayout"
@expand-change="handleExpandChange"
>
<template #first>
<div :class="`h-full min-w-[500px] px-[24px] pb-[16px] ${activeLayout === 'horizontal' ? ' pr-[16px]' : ''}`">
<a-tabs v-model:active-key="contentTab" class="no-content">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs>
<a-divider margin="0" class="!mb-[16px]"></a-divider>
<debugHeader :params="activeDebug.params" :layout="activeLayout" :second-box-height="secondBoxHeight" />
</div>
</template>
<template #second>
<div class="min-w-[290px] bg-[var(--color-text-n9)] p-[8px_16px]">
<div class="flex items-center">
<template v-if="activeLayout === 'vertical'">
<MsButton
v-if="isExpanded"
type="icon"
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
@click="changeExpand(false)"
>
<icon-down :size="12" />
</MsButton>
<MsButton v-else type="icon" status="secondary" class="!mr-0 !rounded-full" @click="changeExpand(true)">
<icon-right :size="12" />
</MsButton>
</template>
<div class="ml-[4px] mr-[24px] font-medium">{{ t('ms.apiTestDebug.responseContent') }}</div>
<a-radio-group
v-model:model-value="activeLayout"
type="button"
size="small"
@change="handleActiveLayoutChange"
>
<a-radio value="vertical">{{ t('ms.apiTestDebug.vertical') }}</a-radio>
<a-radio value="horizontal">{{ t('ms.apiTestDebug.horizontal') }}</a-radio>
</a-radio-group>
</div>
</div>
</template>
</MsSplitBox>
</div>
</template>
<script setup lang="ts">
import { debounce } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import apiMethodName from '../../../components/apiMethodName.vue';
import debugHeader from './header.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestComposition, RequestMethods } from '@/enums/apiEnum';
const { t } = useI18n();
const initDefaultId = `debug-${Date.now()}`;
const activeTab = ref<string | number>(initDefaultId);
const debugTabs = ref<TabItem[]>([
{
id: initDefaultId,
label: t('ms.apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSave: true,
params: [],
},
]);
const debugUrl = ref('');
const activeDebug = ref<TabItem>(debugTabs.value[0]);
function setActiveDebug(item: TabItem) {
activeDebug.value = item;
}
function addDebugTab() {
const id = `debug-${Date.now()}`;
debugTabs.value.push({
id,
label: t('ms.apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSave: true,
params: [],
});
activeTab.value = id;
}
function closeDebugTab(tab: TabItem) {
const index = debugTabs.value.findIndex((item) => item.id === tab.id);
if (activeTab.value === tab.id) {
activeTab.value = debugTabs.value[0]?.id || '';
}
debugTabs.value.splice(index, 1);
}
const moreActionList = [
{
key: 'add',
label: t('common.add'),
},
{
key: 'delete',
label: t('common.delete'),
},
];
const contentTab = ref(RequestComposition.HEADER);
const contentTabList = [
{
value: RequestComposition.HEADER,
label: t('ms.apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('ms.apiTestDebug.body'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
{
value: RequestComposition.PREFIX,
label: t('ms.apiTestDebug.prefix'),
},
{
value: RequestComposition.POST_CONDITION,
label: t('ms.apiTestDebug.postCondition'),
},
{
value: RequestComposition.ASSERTION,
label: t('ms.apiTestDebug.assertion'),
},
{
value: RequestComposition.AUTH,
label: t('ms.apiTestDebug.auth'),
},
{
value: RequestComposition.SETTING,
label: t('ms.apiTestDebug.setting'),
},
];
const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0);
watch(
() => splitBoxSize.value,
debounce((val) => {
if (splitContainerRef.value) {
secondBoxHeight.value = splitContainerRef.value.clientHeight * (1 - val);
}
}, 300),
{
immediate: true,
}
);
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isExpanded = ref(true);
function handleExpandChange(val: boolean) {
isExpanded.value = val;
}
function changeExpand(val: boolean) {
isExpanded.value = val;
if (val) {
splitBoxRef.value?.expand(0.6);
} else {
splitBoxRef.value?.collapse(splitContainerRef.value ? `${splitContainerRef.value.clientHeight - 42}px` : 0);
}
}
function handleActiveLayoutChange() {
isExpanded.value = true;
splitBoxSize.value = 0.6;
}
</script>
<style lang="less" scoped>
.exec-btn {
margin-right: 12px;
:deep(.arco-btn) {
color: white !important;
background-color: rgb(var(--primary-5)) !important;
.btn-base-primary-hover();
.btn-base-primary-active();
.btn-base-primary-disabled();
}
}
:deep(.no-content) {
.arco-tabs-content {
display: none;
}
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<a-popover position="tl" :disabled="!props.desc || props.desc.trim() === ''" class="ms-params-input-popover">
<template #content>
<div class="param-popover-title">
{{ t('ms.apiTestDebug.desc') }}
</div>
<div class="param-popover-value">
{{ props.desc }}
</div>
</template>
<a-input ref="inputRef" v-model:model-value="innerValue" class="param-input" @input="(val) => emit('input', val)" />
</a-popover>
</template>
<script setup lang="ts">
import { useEventListener, useVModel } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
desc: string;
}>();
const emit = defineEmits<{
(e: 'update:value', val: string): void;
(e: 'input', val: string): void;
(e: 'dblclick'): void;
}>();
const { t } = useI18n();
const innerValue = useVModel(props, 'desc', emit);
const inputRef = ref<HTMLElement>();
onMounted(() => {
useEventListener(inputRef.value, 'dblclick', () => {
emit('dblclick');
});
});
</script>
<style lang="less" scoped>
.param-input:not(.arco-input-focus) {
&:not(:hover) {
border-color: transparent;
}
}
.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,356 @@
<template>
<div>
<div class="mb-[8px] flex items-center gap-[8px]">
<a-select v-model:model-value="moduleProtocol" :options="moduleProtocolOptions" class="w-[90px]"></a-select>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear
/>
</div>
<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-count">({{ allFileCount }})</div>
</div>
<div class="ml-auto flex items-center">
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</popConfirm>
</div>
</div>
<a-divider v-if="!props.isModal" class="my-[8px]" />
<a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:data="folderTree"
:keyword="moduleKeyword"
:node-more-actions="folderMoreActions"
:default-expand-all="isExpandAll"
:expand-all="isExpandAll"
:empty-text="t('ms.apiTestDebug.noMatchModule')"
:draggable="!props.isModal"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
:selectable="false"
block-node
title-tooltip-position="left"
@more-action-select="handleFolderMoreSelect"
@more-actions-close="moreActionsClose"
@drop="handleDrop"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div>
</template>
<template v-if="!props.isModal" #extra="nodeData">
<!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 -->
<popConfirm
v-if="nodeData.id !== 'root'"
mode="add"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:parent-id="nodeData.id"
@close="resetFocusNodeKey"
@add-finish="() => initModules()"
>
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusNodeKey(nodeData)">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</popConfirm>
<popConfirm
v-if="nodeData.id !== 'root'"
mode="rename"
:parent-id="nodeData.id"
:node-id="nodeData.id"
:field-config="{ field: renameFolderTitle }"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
@close="resetFocusNodeKey"
@rename-finish="initModules"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</popConfirm>
</template>
</MsTree>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import popConfirm from './popConfirm.vue';
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const props = defineProps<{
isModal?: boolean; //
modulesCount?: Record<string, number>; //
isExpandAll?: boolean; //
}>();
const emit = defineEmits(['init', 'folderNodeSelect']);
const appStore = useAppStore();
const { t } = useI18n();
const { openModal } = useModal();
const virtualListProps = computed(() => {
if (props.isModal) {
return {
height: 'calc(60vh - 190px)',
};
}
return {
height: 'calc(100vh - 325px)',
};
});
const activeFolder = ref<string>('all');
const allFileCount = ref(0);
const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); //
watch(
() => props.isExpandAll,
(val) => {
isExpandAll.value = val;
}
);
function changeExpand() {
isExpandAll.value = !isExpandAll.value;
}
const moduleProtocol = ref('http');
const moduleProtocolOptions = ref([
{
label: 'HTTP',
value: 'http',
},
]);
const moduleKeyword = ref('');
const folderTree = ref<ModuleTreeNode[]>([]);
const focusNodeKey = ref<string | number>('');
const loading = ref(false);
function setFocusNodeKey(node: MsTreeNodeData) {
focusNodeKey.value = node.id || '';
}
const folderMoreActions: ActionsItem[] = [
{
label: 'common.rename',
eventTag: 'rename',
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
},
];
const renamePopVisible = ref(false);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules() {
try {
loading.value = true;
const res = await getReviewModules(appStore.currentProjectId);
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: e.id !== 'root' && !props.isModal,
disabled: e.id === activeFolder.value && props.isModal,
};
});
emit('init', folderTree.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
/**
* 删除文件夹
* @param node 节点信息
*/
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'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
try {
await deleteReviewModule(node.id);
Message.success(t('caseManagement.caseReview.deleteSuccess'));
initModules();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
}
const renameFolderTitle = ref(''); //
function resetFocusNodeKey() {
focusNodeKey.value = '';
renamePopVisible.value = false;
renameFolderTitle.value = '';
}
/**
* 处理树节点更多按钮事件
* @param item
*/
function handleFolderMoreSelect(item: ActionsItem, node: MsTreeNodeData) {
switch (item.eventTag) {
case 'delete':
deleteFolder(node);
resetFocusNodeKey();
break;
case 'rename':
renameFolderTitle.value = node.name || '';
renamePopVisible.value = true;
document.querySelector(`#renameSpan${node.id}`)?.dispatchEvent(new Event('click'));
break;
default:
break;
}
}
/**
* 处理文件夹树节点拖拽事件
* @param tree 树数据
* @param dragNode 拖拽节点
* @param dropNode 释放节点
* @param dropPosition 释放位置
*/
async function handleDrop(
tree: MsTreeNodeData[],
dragNode: MsTreeNodeData,
dropNode: MsTreeNodeData,
dropPosition: number
) {
try {
loading.value = true;
await moveReviewModule({
dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '',
dropPosition,
});
Message.success(t('caseManagement.caseReview.moduleMoveSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
initModules();
}
}
function moreActionsClose() {
if (!renamePopVisible.value) {
// key
resetFocusNodeKey();
}
}
onBeforeMount(() => {
initModules();
});
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
defineExpose({
initModules,
});
</script>
<style lang="less" scoped>
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<a-popconfirm
v-model:popup-visible="innerVisible"
class="ms-pop-confirm--hidden-icon"
position="bottom"
:ok-loading="loading"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="beforeConfirm"
:popup-container="props.popupContainer || 'body'"
@popup-visible-change="reset"
>
<template #content>
<div class="mb-[8px] font-medium">
{{
props.title ||
(props.mode === 'add' ? t('project.fileManagement.addSubModule') : t('project.fileManagement.rename'))
}}
</div>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
class="hidden-item"
field="field"
:rules="[{ required: true, message: t('project.fileManagement.nameNotNull') }, { validator: validateName }]"
>
<a-textarea
v-if="props.fieldConfig?.isTextArea"
v-model:model-value="form.field"
:max-length="props.fieldConfig?.maxLength"
:auto-size="{ maxRows: 4 }"
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
class="w-[245px]"
@press-enter="beforeConfirm(undefined)"
>
</a-textarea>
<a-input
v-else
v-model:model-value="form.field"
:max-length="props.fieldConfig?.maxLength || 50"
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
class="w-[245px]"
@press-enter="beforeConfirm(undefined)"
/>
</a-form-item>
</a-form>
</template>
<slot></slot>
</a-popconfirm>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { addReviewModule, updateReviewModule } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { FieldRule, FormInstance } from '@arco-design/web-vue';
interface FieldConfig {
field?: string;
rules?: FieldRule[];
placeholder?: string;
maxLength?: number;
isTextArea?: boolean;
}
const props = defineProps<{
mode: 'add' | 'rename' | 'fileRename' | 'fileUpdateDesc' | 'repositoryRename';
visible?: boolean;
title?: string;
allNames: string[];
popupContainer?: string;
fieldConfig?: FieldConfig;
parentId?: string; // id
nodeId?: string; // id
}>();
const emit = defineEmits(['update:visible', 'close', 'addFinish', 'renameFinish', 'updateDescFinish']);
const appStore = useAppStore();
const { t } = useI18n();
const innerVisible = ref(props.visible || false);
const form = ref({
field: props.fieldConfig?.field || '',
});
const formRef = ref<FormInstance>();
const loading = ref(false);
watch(
() => props.fieldConfig?.field,
(val) => {
form.value.field = val || '';
},
{
deep: true,
}
);
watch(
() => props.visible,
(val) => {
innerVisible.value = val;
}
);
watch(
() => innerVisible.value,
(val) => {
if (!val) {
emit('close');
}
emit('update:visible', val);
}
);
function beforeConfirm(done?: (closed: boolean) => void) {
if (loading.value) return;
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
loading.value = true;
if (props.mode === 'add') {
//
await addReviewModule({
projectId: appStore.currentProjectId,
parentId: props.parentId || '',
name: form.value.field,
});
Message.success(t('project.fileManagement.addSubModuleSuccess'));
emit('addFinish', form.value.field);
} else if (props.mode === 'rename') {
//
await updateReviewModule({
id: props.nodeId || '',
name: form.value.field,
});
Message.success(t('project.fileManagement.renameSuccess'));
emit('renameFinish', form.value.field);
}
if (done) {
done(true);
} else {
innerVisible.value = false;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
if (done) {
done(false);
}
} finally {
loading.value = false;
}
} else if (done) {
done(false);
}
});
}
function validateName(value: any, callback: (error?: string | undefined) => void) {
if (props.allNames.includes(value)) {
callback(t('project.fileManagement.nameExist'));
}
}
function reset(val: boolean) {
if (!val) {
form.value.field = '';
formRef.value?.resetFields();
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,46 @@
<template>
<MsCard simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5">
<template #first>
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
<a-button type="primary" class="mr-[12px]">{{ t('ms.apiTestDebug.createDebug') }}</a-button>
<a-button type="outline">{{ t('common.import') }}</a-button>
</div>
<div class="px-[24px] py-[16px]">
<moduleTree />
</div>
</template>
<template #second>
<div class="flex h-full flex-col">
<debug />
</div>
</template>
</MsSplitBox>
</MsCard>
</template>
<script lang="ts" setup>
import MsCard from '@/components/pure/ms-card/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import debug from './components/debug/index.vue';
import moduleTree from './components/moduleTree.vue';
import { useI18n } from '@/hooks/useI18n';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
const { t } = useI18n();
function saveDebug() {
console.log('save');
}
onMounted(() => {
registerCatchSaveShortcut(saveDebug);
});
onBeforeUnmount(() => {
removeCatchSaveShortcut(saveDebug);
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,31 @@
export default {
'ms.apiTestDebug.createDebug': 'New debug',
'ms.apiTestDebug.newApi': 'New request',
'ms.apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https',
'ms.apiTestDebug.serverExec': 'Server execution',
'ms.apiTestDebug.localExec': 'Local execution',
'ms.apiTestDebug.noMatchModule': 'No matching module data yet',
'ms.apiTestDebug.header': 'Header',
'ms.apiTestDebug.body': 'Body',
'ms.apiTestDebug.prefix': 'Prefix',
'ms.apiTestDebug.postCondition': 'Post condition',
'ms.apiTestDebug.assertion': 'Assertion',
'ms.apiTestDebug.auth': 'Auth',
'ms.apiTestDebug.setting': 'Setting',
'ms.apiTestDebug.batchAdd': 'Batch add',
'ms.apiTestDebug.responseContent': 'Response content',
'ms.apiTestDebug.vertical': 'Vertical layout',
'ms.apiTestDebug.horizontal': 'Horizontal layout',
'ms.apiTestDebug.paramName': 'Parameter name',
'ms.apiTestDebug.paramNamePlaceholder': 'Please enter parameter name',
'ms.apiTestDebug.paramValue': 'Parameter value',
'ms.apiTestDebug.paramValuePlaceholder': 'Starting with {at}, double-click to quickly enter',
'ms.apiTestDebug.paramValuePreview': 'Parameter preview',
'ms.apiTestDebug.desc': 'Description',
'ms.apiTestDebug.apply': 'Apply',
'ms.apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural',
'ms.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.',
'ms.apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.',
'ms.apiTestDebug.descPlaceholder': 'Please enter content',
};

View File

@ -0,0 +1,30 @@
export default {
'ms.apiTestDebug.createDebug': '新建调试',
'ms.apiTestDebug.newApi': '新建请求',
'ms.apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL',
'ms.apiTestDebug.serverExec': '服务端执行',
'ms.apiTestDebug.localExec': '本地执行',
'ms.apiTestDebug.noMatchModule': '暂无匹配的模块数据',
'ms.apiTestDebug.header': '请求头',
'ms.apiTestDebug.body': '请求体',
'ms.apiTestDebug.prefix': '前置',
'ms.apiTestDebug.postCondition': '后置',
'ms.apiTestDebug.assertion': '断言',
'ms.apiTestDebug.auth': '认证',
'ms.apiTestDebug.setting': '设置',
'ms.apiTestDebug.batchAdd': '批量添加',
'ms.apiTestDebug.responseContent': '响应内容',
'ms.apiTestDebug.vertical': '上下布局',
'ms.apiTestDebug.horizontal': '左右布局',
'ms.apiTestDebug.paramName': '参数名称',
'ms.apiTestDebug.paramNamePlaceholder': '请输入参数名称',
'ms.apiTestDebug.paramValue': '参数值',
'ms.apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入',
'ms.apiTestDebug.paramValuePreview': '参数预览',
'ms.apiTestDebug.desc': '描述',
'ms.apiTestDebug.apply': '应用',
'ms.apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural',
'ms.apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据',
'ms.apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等',
'ms.apiTestDebug.descPlaceholder': '请输入内容',
};

View File

@ -1,175 +0,0 @@
<template>
<div class="h-[100vh] bg-white px-[20px] py-[16px] pb-0">
<ms-base-table v-bind="propsRes" :action-config="actionConfig" v-on="propsEvent"> </ms-base-table>
</div>
<a-divider />
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { BatchActionConfig, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getTableList } from '@/api/modules/api-test/index';
import { useTableStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
filterable: {
filters: [
{
text: '> 20000',
value: '20000',
},
{
text: '> 30000',
value: '30000',
},
],
filter: (value, record) => record.salary > value,
multiple: true,
},
},
{
title: '接口名称',
dataIndex: 'name',
width: 200,
},
{
title: '请求类型',
dataIndex: 'method',
},
{
title: '责任人',
dataIndex: 'username',
},
{
title: '路径',
dataIndex: 'path',
},
{
title: '标签',
dataIndex: 'tags',
},
{
title: '更新时间',
dataIndex: 'updateTime',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '用例数',
dataIndex: 'caseTotal',
},
{
title: '用例状态',
dataIndex: 'caseStatus',
},
{
title: '用例通过率',
dataIndex: 'casePassingRate',
},
{
title: '接口状态',
dataIndex: 'status',
},
{
title: '创建时间',
slotName: 'createTime',
width: 200,
},
{
title: '描述',
dataIndex: 'description',
},
{
title: '操作',
slotName: 'action',
fixed: 'right',
width: 200,
},
];
const actionConfig: BatchActionConfig = {
baseAction: [
{
label: 'msTable.batch.export',
eventTag: 'batchExport',
isDivider: false,
danger: false,
},
{
label: 'msTable.batch.edit',
eventTag: 'batchEdit',
isDivider: false,
danger: false,
},
{
label: 'msTable.batch.moveTo',
eventTag: 'batchMoveTo',
isDivider: false,
danger: false,
},
{
label: 'msTable.batch.copyTo',
eventTag: 'batchCopyTo',
isDivider: false,
danger: false,
},
],
moreAction: [
{
label: 'msTable.batch.related',
eventTag: 'batchRelated',
isDivider: false,
danger: false,
},
{
label: 'msTable.batch.generateDep',
eventTag: 'batchGenerate',
isDivider: false,
danger: false,
},
{
label: 'msTable.batch.addPublic',
eventTag: 'batchAddTo',
isDivider: false,
danger: false,
},
{
isDivider: true,
},
{
label: 'msTable.batch.delete',
eventTag: 'batchDelete',
isDivider: false,
danger: true,
},
],
};
const tableStore = useTableStore();
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
const { propsRes, propsEvent, loadList } = useTable(getTableList, {
columns,
scroll: { y: 750, x: 2000 },
selectable: true,
});
const fetchData = async () => {
await loadList();
};
onMounted(() => {
fetchData();
});
</script>

View File

@ -1,20 +0,0 @@
export default {
apiTest: {
id: 'ID',
name: 'Api name',
method: 'Requset Method',
path: 'Request Path',
description: 'Api description',
createTime: 'Create time',
updateTime: 'Update time',
operation: 'Operation',
addApiTest: 'Add Api',
editApiTest: 'Edit api',
deleteApiTest: 'Delete api',
deleteApiTestConfirm: 'Are you sure to delete this api?',
deleteApiTestSuccess: 'Delete api success!',
deleteApiTestError: 'Delete api failed, please try again!',
addApiTestSuccess: 'Add api success!',
addApiTestError: 'Add api failed, please try again!',
},
};

View File

@ -1,20 +0,0 @@
export default {
apiTest: {
id: 'ID',
name: '接口名称',
method: '请求方式',
path: '请求路径',
description: '接口描述',
createTime: '创建时间',
updateTime: '更新时间',
operation: '操作',
addApiTest: '新增接口',
editApiTest: '编辑接口',
deleteApiTest: '删除接口',
deleteApiTestConfirm: '确定删除该接口吗?',
deleteApiTestSuccess: '删除接口成功!',
deleteApiTestError: '删除接口失败,请重试!',
addApiTestSuccess: '新增接口成功!',
addApiTestError: '新增接口失败,请重试!',
},
};

View File

@ -75,7 +75,7 @@
<template #default>
<div ref="wrapperRef" class="h-full bg-white">
<MsSplitBox ref="wrapperRef" expand-direction="right" :max="0.7" :min="0.7" :size="900">
<template #left>
<template #first>
<div class="leftWrapper h-full">
<div class="header h-[50px]">
<a-tabs v-model:active-key="activeTab">
@ -92,7 +92,7 @@
</div>
</div>
</template>
<template #right>
<template #second>
<div class="rightWrapper p-[24px]">
<div class="mb-4 font-medium">{{ t('caseManagement.featureCase.basicInfo') }}</div>
<div class="baseItem">

View File

@ -85,7 +85,7 @@
<template #default>
<div ref="wrapperRef" class="h-full bg-white">
<MsSplitBox ref="wrapperRef" expand-direction="right" :max="0.7" :min="0.7" :size="900">
<template #left>
<template #first>
<div class="leftWrapper h-full">
<div class="header h-[50px]">
<a-menu mode="horizontal" :default-selected-keys="[activeTab]" @menu-item-click="clickMenu">
@ -126,7 +126,7 @@
</div>
</div>
</template>
<template #right>
<template #second>
<div class="rightWrapper p-[24px]">
<div class="mb-4 font-medium">{{ t('caseManagement.featureCase.basicInfo') }}</div>
<div class="baseItem">

View File

@ -1,7 +1,7 @@
<template>
<div class="pageWrap">
<MsSplitBox>
<template #left>
<template #first>
<div class="p-[24px]">
<a-input-search
v-model:model-value="groupKeyword"
@ -60,7 +60,7 @@
</div>
</div>
</template>
<template #right>
<template #second>
<div class="p-[24px]">
<div class="page-header mb-4 h-[34px]">
<div class="text-[var(--color-text-1)]"

View File

@ -14,7 +14,7 @@
<a-divider class="!my-0" />
<div class="pageWrap">
<MsSplitBox>
<template #left>
<template #first>
<div class="p-[24px] pb-0">
<div class="feature-case h-[100%]">
<div class="case h-[38px]">
@ -80,7 +80,7 @@
</div>
</div>
</template>
<template #right>
<template #second>
<div class="p-[24px]">
<CaseTable
:active-folder="activeFolder"

View File

@ -39,7 +39,7 @@
>
<div class="mb-[4px] flex items-center justify-between">
<div>{{ item.id }}</div>
<div class="flex items-center gap-[4px]">
<div class="flex items-center gap-[4px] leading-[22px]">
<MsIcon
:type="resultMap[item.result as ResultMap].icon"
:style="{color: resultMap[item.result as ResultMap].color}"
@ -127,10 +127,10 @@
<MsDescription v-if="showTab === 'baseInfo'" :descriptions="descriptions" label-width="90px" />
<div v-else-if="showTab === 'detail'" class="h-full">
<MsSplitBox :size="0.8" direction="vertical" min="0" :max="0.99">
<template #top>
<template #first>
<caseTabDetail :form="detailForm" :allow-edit="false" />
</template>
<template #bottom>
<template #second>
<div class="flex h-full flex-col overflow-hidden">
<div class="mb-[8px] font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.reviewHistory') }}

View File

@ -115,7 +115,6 @@
const props = defineProps<{
isModal?: boolean; //
modulesCount?: Record<string, number>; //
showType?: string; //
isExpandAll?: boolean; //
}>();
const emit = defineEmits(['init', 'folderNodeSelect']);

View File

@ -86,12 +86,12 @@
</MsCard>
<MsCard class="mt-[16px]" :special-height="180" simple has-breadcrumb no-content-padding>
<MsSplitBox>
<template #left>
<template #first>
<div class="p-[24px]">
<CaseTree ref="folderTreeRef" @folder-node-select="handleFolderNodeSelect" />
</div>
</template>
<template #right>
<template #second>
<CaseTable :active-folder="activeFolderId"></CaseTable>
</template>
</MsSplitBox>

View File

@ -4,18 +4,18 @@
<a-button type="primary" @click="goCreateReview">{{ t('caseManagement.caseReview.create') }}</a-button>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="all">{{ t('common.all') }}</a-radio>
<a-radio value="wait">{{ t('caseManagement.caseReview.waitMyReview') }}</a-radio>
<a-radio value="create">{{ t('caseManagement.caseReview.myCreate') }}</a-radio>
<a-radio value="reviewByMe">{{ t('caseManagement.caseReview.waitMyReview') }}</a-radio>
<a-radio value="createByMe">{{ t('caseManagement.caseReview.myCreate') }}</a-radio>
</a-radio-group>
</div>
<div class="relative h-[calc(100%-73px)]">
<MsSplitBox>
<template #left>
<template #first>
<div class="px-[24px] py-[16px]">
<ModuleTree ref="folderTreeRef" @folder-node-select="handleFolderNodeSelect" @init="initModuleTree" />
</div>
</template>
<template #right>
<template #second>
<ReviewTable :active-folder="activeFolderId" :module-tree="moduleTree" @go-create="goCreateReview" />
</template>
</MsSplitBox>
@ -42,7 +42,7 @@
const router = useRouter();
const { t } = useI18n();
type ShowType = 'all' | 'wait' | 'create';
type ShowType = 'all' | 'reviewByMe' | 'createByMe';
const showType = ref<ShowType>('all');

View File

@ -1,6 +1,6 @@
export default {
'caseManagement.caseReview.create': 'Create review',
'caseManagement.caseReview.waitMyReview': 'Awaiting my review',
'caseManagement.caseReview.waitMyReview': 'I reviewed',
'caseManagement.caseReview.myCreate': 'I created',
'caseManagement.caseReview.searchPlaceholder': 'Search by ID or name',
'caseManagement.caseReview.archive': 'Archive',
@ -130,4 +130,6 @@ export default {
'caseManagement.caseReview.reviewHistory': 'Review history',
'caseManagement.caseReview.noMatchReviewer': 'No matching handler, can be set in {menu}',
'caseManagement.caseReview.crateCase': 'Create case',
'caseManagement.caseReview.demandCases': 'Requirements association list',
'caseManagement.caseReview.demandSearchPlaceholder': 'Search by name',
};

View File

@ -1,6 +1,6 @@
export default {
'caseManagement.caseReview.create': '创建评审',
'caseManagement.caseReview.waitMyReview': '我评审',
'caseManagement.caseReview.waitMyReview': '我评审',
'caseManagement.caseReview.myCreate': '我创建的',
'caseManagement.caseReview.searchPlaceholder': '通过 ID 或名称搜索',
'caseManagement.caseReview.archive': '归档',

View File

@ -1255,7 +1255,7 @@
.card-list {
@apply grid flex-1 overflow-auto;
.ms-scroll-bar();
.ms-container--shadow();
.ms-container--shadow-y();
gap: 24px;
grid-template-columns: repeat(auto-fill, minmax(102px, 1fr));

View File

@ -1,7 +1,7 @@
<template>
<div class="page">
<MsSplitBox>
<template #left>
<template #first>
<div class="p-[24px]">
<div class="folder" @click="setActiveFolder('my')">
<div :class="getFolderClass('my')">
@ -74,7 +74,7 @@
</div>
</div>
</template>
<template #right>
<template #second>
<rightBox
:active-folder="activeFolder"
:active-folder-type="activeFolderType"

View File

@ -514,7 +514,7 @@
@apply relative;
background-color: var(--color-text-n9);
.ms-container--shadow();
.ms-container--shadow-y();
.robot-list {
@apply grid max-h-full overflow-y-auto;
.ms-scroll-bar();

View File

@ -312,7 +312,7 @@
<style lang="less" scoped>
.field-out-container {
@apply h-full;
.ms-container--shadow();
.ms-container--shadow-y();
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);

View File

@ -1,10 +1,10 @@
<template>
<div class="card">
<MsSplitBox v-model:width="leftWidth" @expand-change="handleCollapse">
<template #left>
<template #first>
<UserGroupLeft ref="ugLeftRef" @handle-select="handleSelect" @add-user-success="handleAddMember" />
</template>
<template #right>
<template #second>
<div class="p-[24px]">
<div class="flex flex-row items-center justify-between">
<a-tooltip :content="currentUserGroupItem.name">

View File

@ -27,7 +27,7 @@
<a-select
v-model:model-value="activeTime"
:options="timeOptions"
class="time-input-append"
class="select-input-append"
:loading="saveLoading"
@change="() => saveConfig()"
/>
@ -124,18 +124,5 @@
</script>
<style lang="less" scoped>
:deep(.arco-input-append) {
@apply border-none;
}
:deep(.time-input-append) {
@apply z-10;
margin-left: -16px !important;
border-radius: 0 4px 4px 0 !important;
background-color: var(--color-text-n8) !important;
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: var(--color-text-n8) !important;
}
}
.ms-input-group--append();
</style>

View File

@ -1,10 +1,10 @@
<template>
<div class="card">
<MsSplitBox v-model:width="leftWidth" @expand-change="handleCollapse">
<template #left>
<template #first>
<UserGroupLeft ref="ugLeftRef" @handle-select="handleSelect" @add-user-success="handleAddMember" />
</template>
<template #right>
<template #second>
<div class="p-[24px]">
<div class="flex flex-row items-center justify-between">
<a-tooltip :content="currentUserGroupItem.name">