feat(接口测试): pdf展示&部分 mock 页面
This commit is contained in:
parent
10e3a7af03
commit
03bbdbf15e
|
@ -1,7 +1,7 @@
|
|||
@font-face {
|
||||
font-family: iconfont; /* Project id 3462279 */
|
||||
src: url('iconfont.woff2?t=1711511079663') format('woff2'), url('iconfont.woff?t=1711511079663') format('woff'),
|
||||
url('iconfont.ttf?t=1711511079663') format('truetype'), url('iconfont.svg?t=1711511079663#iconfont') format('svg');
|
||||
src: url('iconfont.woff2?t=1714372635707') format('woff2'), url('iconfont.woff?t=1714372635707') format('woff'),
|
||||
url('iconfont.ttf?t=1714372635707') format('truetype'), url('iconfont.svg?t=1714372635707#iconfont') format('svg');
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 16px;
|
||||
|
@ -10,6 +10,12 @@
|
|||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.icon-icon_menu_unfold::before {
|
||||
content: '\e7a9';
|
||||
}
|
||||
.icon-icon_menu_fold::before {
|
||||
content: '\e7aa';
|
||||
}
|
||||
.icon-icon_stop::before {
|
||||
content: '\e7a8';
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,20 @@
|
|||
"css_prefix_text": "icon-",
|
||||
"description": "DE、MS项目icon管理",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "39725024",
|
||||
"name": "icon_menu_unfold",
|
||||
"font_class": "icon_menu_unfold",
|
||||
"unicode": "e7a9",
|
||||
"unicode_decimal": 59305
|
||||
},
|
||||
{
|
||||
"icon_id": "39725023",
|
||||
"name": "icon_menu_fold",
|
||||
"font_class": "icon_menu_fold",
|
||||
"unicode": "e7aa",
|
||||
"unicode_decimal": 59306
|
||||
},
|
||||
{
|
||||
"icon_id": "39710057",
|
||||
"name": "icon_stop",
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
/>
|
||||
<missing-glyph />
|
||||
|
||||
<glyph glyph-name="icon_menu_unfold" unicode="" d="M853.333333 714.666667a42.666667 42.666667 0 0 0 0-85.333334H170.666667a42.666667 42.666667 0 1 0 0 85.333334h682.666666z m0-192a42.666667 42.666667 0 0 0 0-85.333334h-341.333333a42.666667 42.666667 0 0 0 0 85.333334h341.333333z m0-192a42.666667 42.666667 0 0 0 0-85.333334h-341.333333a42.666667 42.666667 0 0 0 0 85.333334h341.333333z m0-192a42.666667 42.666667 0 0 0 0-85.333334H170.666667a42.666667 42.666667 0 0 0 0 85.333334h682.666666zM318.72 526.848A42.666667 42.666667 0 0 0 384 490.666667v-213.333334a42.666667 42.666667 0 0 0-65.28-36.181333l-170.666667 106.666667a42.666667 42.666667 0 0 0 0 72.362666l170.666667 106.666667zM298.666667 413.738667L251.093333 384 298.666667 354.304v59.434667z" horiz-adv-x="1024" />
|
||||
|
||||
<glyph glyph-name="icon_menu_fold" unicode="" d="M853.333333 714.666667a42.666667 42.666667 0 0 0 0-85.333334H170.666667a42.666667 42.666667 0 1 0 0 85.333334h682.666666z m0-192a42.666667 42.666667 0 0 0 0-85.333334h-341.333333a42.666667 42.666667 0 0 0 0 85.333334h341.333333z m0-192a42.666667 42.666667 0 0 0 0-85.333334h-341.333333a42.666667 42.666667 0 0 0 0 85.333334h341.333333z m0-192a42.666667 42.666667 0 0 0 0-85.333334H170.666667a42.666667 42.666667 0 0 0 0 85.333334h682.666666zM128 490.666667a42.666667 42.666667 0 0 0 65.28 36.181333l170.666667-106.666667a42.666667 42.666667 0 0 0 0-72.362666l-170.666667-106.666667A42.666667 42.666667 0 0 0 128 277.333333v213.333334z m85.333333-76.928v-59.434667l47.530667 29.738667L213.333333 413.738667z" horiz-adv-x="1024" />
|
||||
|
||||
<glyph glyph-name="icon_stop" unicode="" 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 768zM405.333333 554.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-256a42.666667 42.666667 0 0 0-85.333333 0V512a42.666667 42.666667 0 0 0 42.666666 42.666667z m213.333334 0a42.666667 42.666667 0 0 0 42.666666-42.666667v-256a42.666667 42.666667 0 0 0-85.333333 0V512a42.666667 42.666667 0 0 0 42.666667 42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
<glyph glyph-name="icon_env1" unicode="" d="M399.701333 810.666667a42.666667 42.666667 0 0 0 32.085334-14.506667L531.328 682.666667h362.410667c45.738667 0 83.968-34.346667 87.338666-78.933334L981.333333 597.333333v-554.666666c0-47.616-39.68-85.333333-87.594666-85.333334H130.261333C82.346667-42.666667 42.666667-4.949333 42.666667 42.666667V725.333333c0 47.616 39.68 85.333333 87.594666 85.333334h269.44z m-19.328-85.333334H130.261333C128.554667 725.333333 128 724.821333 128 725.333333v-682.666666c0 0.512 0.512 0 2.261333 0h763.477334c1.706667 0 2.261333 0.512 2.261333 0V597.333333c0-0.512-0.512 0-2.261333 0H512a42.666667 42.666667 0 0 0-32.085333 14.506667L380.373333 725.333333z m8.362667-279.808v-58.666666H246.186667v-43.690667h132.181333v-56.064H246.186667v-54.186667h146.602666V170.666667H161.109333v274.858666H388.693333z m187.861333-71.253333c21.12 0 37.632-6.272 49.578667-18.816 11.946667-12.586667 17.92-32 17.92-58.24V170.666667h-76.672v109.482666c0 12.501333-2.346667 21.333333-6.954667 26.538667-4.608 5.205333-11.093333 7.808-19.498666 7.808a27.733333 27.733333 0 0 1-22.485334-10.496c-5.76-7.04-8.661333-19.584-8.661333-37.717333V170.666667h-76.288v199.125333h71.04v-32.426667c10.666667 13.226667 21.376 22.698667 32.256 28.416 10.88 5.674667 24.149333 8.533333 39.765333 8.533334z m159.573334-4.48l38.613333-126.208 39.936 126.208h77.056L807.253333 170.666667h-67.669333l-82.901333 199.125333h79.530666z" horiz-adv-x="1024" />
|
||||
|
|
Before Width: | Height: | Size: 442 KiB After Width: | Height: | Size: 444 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -20,7 +20,7 @@
|
|||
import { getFirstRouterNameByCurrentRoute } from '@/utils/permission';
|
||||
import { listenerRouteChange } from '@/utils/route-listener';
|
||||
|
||||
import { ProjectManagementRouteEnum, SettingRouteEnum } from '@/enums/routeEnum';
|
||||
import { ProjectManagementRouteEnum, RouteEnum, SettingRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import useMenuTree from './use-menu-tree';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
|
@ -123,6 +123,11 @@
|
|||
|
||||
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]];
|
||||
}
|
||||
if (newRoute.fullPath.includes(RouteEnum.SETTING)) {
|
||||
appStore.updateSettings({ menuCollapse: false });
|
||||
} else {
|
||||
appStore.updateSettings({ menuCollapse: true });
|
||||
}
|
||||
}, true);
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });
|
||||
|
|
|
@ -195,7 +195,7 @@
|
|||
class="no-content relative mt-[8px] border-b"
|
||||
/>
|
||||
</div>
|
||||
<div ref="splitContainerRef" class="request-and-response h-[calc(100%-100px)]">
|
||||
<div ref="splitContainerRef" class="request-and-response h-[calc(100%-92px)]">
|
||||
<MsSplitBox
|
||||
ref="verticalSplitBoxRef"
|
||||
v-model:size="splitBoxSize"
|
||||
|
@ -991,8 +991,12 @@
|
|||
const saveModalFormRef = ref<FormInstance>();
|
||||
const saveLoading = ref(false);
|
||||
const selectTree = computed(() => {
|
||||
if (saveModalVisible.value || (!props.isCase && props.isDefinition && saveModalVisible.value)) {
|
||||
// 调试模式打开保存弹窗,或者是接口定义模式下打开保存弹窗才进行计算,避免大数据量导致进入时就计算卡顿 TODO:worker线程处理计算任务
|
||||
if (
|
||||
requestVModel.value.activeTab === RequestComposition.BASE_INFO ||
|
||||
saveModalVisible.value ||
|
||||
(!props.isCase && props.isDefinition && saveModalVisible.value)
|
||||
) {
|
||||
// 切换到基础信息 tab、调试模式打开保存弹窗,或者是接口定义模式下打开保存弹窗才进行计算,避免大数据量导致进入时就计算卡顿 TODO:worker线程处理计算任务
|
||||
return filterTree(cloneDeep(props.moduleTree || []), (e) => {
|
||||
e.draggable = false;
|
||||
return e.type === 'MODULE';
|
||||
|
|
|
@ -222,7 +222,6 @@
|
|||
import { defaultKeyValueParamItem, defaultResponseItem, statusCodes } from '../../config';
|
||||
|
||||
const props = defineProps<{
|
||||
responseDefinition: ResponseDefinition[];
|
||||
uploadTempFileApi?: (file: File) => Promise<any>; // 上传临时文件接口
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
|
@ -240,15 +239,25 @@
|
|||
const responseTabs = defineModel<ResponseItem[]>('responseDefinition', {
|
||||
required: true,
|
||||
});
|
||||
const activeResponse = ref<ResponseItem>(responseTabs.value[0] || defaultResponseItem);
|
||||
const activeResponse = ref<ResponseItem>(responseTabs.value[0] || cloneDeep(defaultResponseItem));
|
||||
|
||||
watch(
|
||||
() => responseTabs.value,
|
||||
(arr) => {
|
||||
if (arr.length > 0) {
|
||||
[activeResponse.value] = arr;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
|
||||
const id = new Date().getTime();
|
||||
responseTabs.value.push({
|
||||
...cloneDeep(defaultResponseItem),
|
||||
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
|
||||
name: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
|
||||
...defaultProps,
|
||||
id: new Date().getTime(),
|
||||
id,
|
||||
defaultFlag: false,
|
||||
showPopConfirm: false,
|
||||
showRenamePopConfirm: false,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div :class="['response-head', props.isExpanded ? '' : 'border-t']">
|
||||
<slot name="titleLeft">
|
||||
<div class="flex items-center justify-between">
|
||||
<template v-if="props.activeLayout === 'vertical'">
|
||||
<template v-if="activeLayout === 'vertical'">
|
||||
<MsButton
|
||||
v-if="props.isExpanded"
|
||||
type="icon"
|
||||
|
@ -44,7 +44,7 @@
|
|||
<div v-else class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
||||
<a-radio-group
|
||||
v-if="!props.hideLayoutSwitch"
|
||||
v-model:model-value="innerLayout"
|
||||
v-model:model-value="activeLayout"
|
||||
type="button"
|
||||
size="small"
|
||||
@change="(val) => emit('changeLayout', val as Direction)"
|
||||
|
@ -62,14 +62,14 @@
|
|||
:class="[isResponseModel ? 'h-[381px] w-full' : 'h-[calc(100%-35px)] w-full px-[16px] pb-[16px]']"
|
||||
>
|
||||
<edit
|
||||
v-if="props.isEdit && activeResponseType === 'content' && innerResponseDefinition"
|
||||
v-model:response-definition="innerResponseDefinition"
|
||||
v-if="props.isEdit && activeResponseType === 'content' && responseDefinition"
|
||||
v-model:response-definition="responseDefinition"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
@change="handleResponseChange"
|
||||
/>
|
||||
<result
|
||||
v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
|
||||
v-model:active-tab="innerActiveTab"
|
||||
v-model:active-tab="activeTab"
|
||||
:request-result="props.requestResult"
|
||||
:console="props.console"
|
||||
:is-http-protocol="props.isHttpProtocol"
|
||||
|
@ -97,13 +97,10 @@
|
|||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activeTab: ResponseComposition;
|
||||
isExpanded?: boolean;
|
||||
isPriorityLocalExec?: boolean;
|
||||
requestUrl?: string;
|
||||
isHttpProtocol?: boolean;
|
||||
activeLayout?: Direction;
|
||||
responseDefinition?: ResponseItem[];
|
||||
requestResult?: RequestResult;
|
||||
console?: string;
|
||||
hideLayoutSwitch?: boolean; // 隐藏布局切换
|
||||
|
@ -117,7 +114,6 @@
|
|||
}>(),
|
||||
{
|
||||
isExpanded: true,
|
||||
activeLayout: 'vertical',
|
||||
hideLayoutSwitch: false,
|
||||
showEmpty: true,
|
||||
}
|
||||
|
@ -131,21 +127,21 @@
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerLayout = defineModel<Direction>('activeLayout', {
|
||||
const activeLayout = defineModel<Direction>('activeLayout', {
|
||||
default: 'vertical',
|
||||
});
|
||||
const innerActiveTab = defineModel<ResponseComposition>('activeTab', {
|
||||
const activeTab = defineModel<ResponseComposition>('activeTab', {
|
||||
required: true,
|
||||
});
|
||||
const innerResponseDefinition = defineModel<ResponseItem[]>('responseDefinition', {
|
||||
const responseDefinition = defineModel<ResponseItem[]>('responseDefinition', {
|
||||
default: [],
|
||||
});
|
||||
watchEffect(() => {
|
||||
// 过滤无效数据后的有效响应数据;当接口导入时会存在部分字段为 null 的数据,需要设置默认值
|
||||
let hasInvalid = false;
|
||||
let validResponseDefinition: ResponseItem[] = [];
|
||||
if (props.responseDefinition && props.responseDefinition.length > 0) {
|
||||
validResponseDefinition = props.responseDefinition.map((item, i) => {
|
||||
if (responseDefinition.value.length > 0) {
|
||||
validResponseDefinition = responseDefinition.value.map((item, i) => {
|
||||
// 某些字段在导入时接口返回 null,需要设置默认值
|
||||
if (!item.headers) {
|
||||
item.headers = [];
|
||||
|
@ -189,7 +185,7 @@
|
|||
});
|
||||
}
|
||||
if (hasInvalid) {
|
||||
innerResponseDefinition.value = validResponseDefinition;
|
||||
responseDefinition.value = validResponseDefinition;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
<template>
|
||||
<div v-if="showImg">
|
||||
<div v-if="showImg || isPdf" :class="showType === 'text' ? '' : 'h-full'">
|
||||
<div class="mb-[8px] flex items-center gap-[16px]">
|
||||
<a-button type="outline" class="arco-btn-outline--secondary" size="mini" @click="handleDownload">
|
||||
{{ t('common.download') }}
|
||||
</a-button>
|
||||
<a-radio-group v-model:model-value="showType" type="button" size="small">
|
||||
<a-radio value="image">{{ t('common.image') }}</a-radio>
|
||||
<a-radio v-if="isPdf" value="pdf">pdf</a-radio>
|
||||
<a-radio v-else value="image">{{ t('common.image') }}</a-radio>
|
||||
<a-radio value="text">{{ t('common.text') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-image v-show="showType === 'image'" :src="imageUrl"></a-image>
|
||||
<object
|
||||
v-if="isPdf && showType === 'pdf'"
|
||||
:data="imageUrl"
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
style="height: calc(100% - 30px)"
|
||||
></object>
|
||||
<a-image v-else-if="showType === 'image'" :src="imageUrl"></a-image>
|
||||
</div>
|
||||
<MsCodeEditor
|
||||
v-show="!showImg || showType === 'text'"
|
||||
v-show="(!showImg && !isPdf) || showType === 'text'"
|
||||
ref="responseEditorRef"
|
||||
:model-value="props.requestResult?.responseResult.body || ''"
|
||||
:language="responseLanguage"
|
||||
|
@ -81,6 +89,12 @@
|
|||
}
|
||||
return false;
|
||||
});
|
||||
const isPdf = computed(() => {
|
||||
if (props.requestResult) {
|
||||
return props.requestResult.responseResult.contentType === 'application/pdf';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const imageUrl = computed(() => {
|
||||
if (props.requestResult) {
|
||||
return `data:${props.requestResult?.responseResult.contentType};base64,${props.requestResult?.responseResult.imageUrl}`;
|
||||
|
@ -88,10 +102,24 @@
|
|||
return '';
|
||||
});
|
||||
|
||||
const showType = ref<'image' | 'text'>('image');
|
||||
const showType = ref<'image' | 'pdf' | 'text'>('image');
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.requestResult) {
|
||||
if (showImg.value) {
|
||||
showType.value = 'image';
|
||||
} else if (isPdf.value) {
|
||||
showType.value = 'pdf';
|
||||
} else {
|
||||
showType.value = 'text';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleDownload() {
|
||||
if (imageUrl.value) {
|
||||
if (isPdf.value) {
|
||||
downloadUrlFile(imageUrl.value, 'response.pdf');
|
||||
} else if (imageUrl.value) {
|
||||
downloadUrlFile(imageUrl.value, `response.${props.requestResult?.responseResult.contentType.split('/')[1]}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,19 @@
|
|||
:member-options="memberOptions"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane v-if="!activeApiTab.isNew" key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane> -->
|
||||
<a-tab-pane
|
||||
v-if="!activeApiTab.isNew && activeApiTab.protocol === 'HTTP'"
|
||||
key="mock"
|
||||
title="MOCK"
|
||||
class="ms-api-tab-pane"
|
||||
>
|
||||
<mockTable
|
||||
:active-module="props.activeModule"
|
||||
:offspring-ids="props.offspringIds"
|
||||
:protocol="activeApiTab.protocol"
|
||||
is-api
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -115,7 +127,6 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import caseTable from '../case/caseTable.vue';
|
||||
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
|
||||
import apiTable from './apiTable.vue';
|
||||
import executeButton from '@/views/api-test/components/executeButton.vue';
|
||||
|
@ -156,6 +167,8 @@
|
|||
() => import('@/views/api-test/components/requestComposition/index.vue')
|
||||
);
|
||||
const preview = defineAsyncComponent(() => import('./preview/index.vue'));
|
||||
const mockTable = defineAsyncComponent(() => import('../mock/mockTable.vue'));
|
||||
const caseTable = defineAsyncComponent(() => import('../case/caseTable.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
activeModule: string;
|
||||
|
|
|
@ -47,9 +47,9 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #num="{ record }">
|
||||
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">{{
|
||||
record.num
|
||||
}}</MsButton>
|
||||
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
|
||||
{{ record.num }}
|
||||
</MsButton>
|
||||
</template>
|
||||
<template #caseLevel="{ record }">
|
||||
<a-select
|
||||
|
|
|
@ -59,6 +59,12 @@
|
|||
:member-options="memberOptions"
|
||||
@delete-case="(id) => handleDeleteApiFromModuleTree(id)"
|
||||
/>
|
||||
<MockTable
|
||||
v-if="activeApiTab.id === 'all' && currentTab === 'mock'"
|
||||
:active-module="props.activeModule"
|
||||
:offspring-ids="props.offspringIds"
|
||||
:protocol="props.protocol"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -71,7 +77,6 @@
|
|||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
// import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
|
||||
import { getProtocolList } from '@/api/modules/api-test/common';
|
||||
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
@ -91,6 +96,10 @@
|
|||
|
||||
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
|
||||
|
||||
const MockTable = defineAsyncComponent(
|
||||
() => import('@/views/api-test/management/components/management/mock/mockTable.vue')
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
activeModule: string;
|
||||
offspringIds: string[];
|
||||
|
@ -113,6 +122,7 @@
|
|||
const tabOptions = [
|
||||
{ label: 'API', value: 'api' },
|
||||
...(hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ']) ? [{ label: 'CASE', value: 'case' }] : []),
|
||||
...(hasAnyPermission(['PROJECT_API_DEFINITION_MOCK:READ']) ? [{ label: 'MOCK', value: 'mock' }] : []),
|
||||
];
|
||||
|
||||
const apiRef = ref<InstanceType<typeof api>>();
|
||||
|
@ -229,7 +239,13 @@
|
|||
|
||||
// 下拉框切换
|
||||
function currentTabChange(val: any) {
|
||||
apiTabs.value[0].label = val === 'api' ? t('apiTestManagement.allApi') : t('case.allCase');
|
||||
if (val === 'api') {
|
||||
apiTabs.value[0].label = t('apiTestManagement.allApi');
|
||||
} else if (val === 'case') {
|
||||
apiTabs.value[0].label = t('case.allCase');
|
||||
} else {
|
||||
apiTabs.value[0].label = t('mockManagement.allMock');
|
||||
}
|
||||
changeActiveApiTabToFirst();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="visible"
|
||||
unmount-on-close
|
||||
:title="t('caseManagement.featureCase.caseDetail')"
|
||||
:width="960"
|
||||
:footer="false"
|
||||
no-content-padding
|
||||
>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="visible"
|
||||
unmount-on-close
|
||||
:title="t('mockManagement.mockDetail')"
|
||||
:width="960"
|
||||
:footer="false"
|
||||
no-content-padding
|
||||
>
|
||||
<template #tbutton>
|
||||
<div class="right-operation-button-icon flex items-center gap-[4px]">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
@click="isEdit = true"
|
||||
>
|
||||
<MsIcon type="icon-icon_edit_outlined" />
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+DELETE']"
|
||||
type="icon"
|
||||
status="danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
|
||||
{{ t('common.delete') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<MsDetailCard :title="`【${mockDetail.num}】${mockDetail.name}`" :description="[]" class="mb-[16px]">
|
||||
<template #titleRight>
|
||||
<div class="flex items-center gap-[16px]">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<div class="whitespace-nowrap text-[var(--color-text-4)]">{{ t('apiTestManagement.apiType') }}</div>
|
||||
<apiMethodName :method="mockDetail.method" tag-size="small" is-tag />
|
||||
</div>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<div class="whitespace-nowrap text-[var(--color-text-4)]">{{ t('apiTestManagement.path') }}</div>
|
||||
<a-tooltip :content="mockDetail.apiPath">
|
||||
<div class="one-line-text">{{ mockDetail.apiPath }}</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsDetailCard>
|
||||
<a-form ref="mockForm" :model="mockDetail">
|
||||
<a-form-item
|
||||
class="hidden-item"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: t('mockManagement.nameNotNull') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="mockDetail.name"
|
||||
:placeholder="t('mockManagement.namePlaceholder')"
|
||||
class="w-[732px]"
|
||||
:disabled="isReadOnly"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item class="hidden-item" :rules="[{ required: true, message: t('mockManagement.nameNotNull') }]">
|
||||
<MsTagsInput
|
||||
v-model:model-value="mockDetail.tags"
|
||||
class="w-[732px]"
|
||||
:placeholder="t('mockManagement.namePlaceholder')"
|
||||
allow-clear
|
||||
unique-value
|
||||
retain-input-value
|
||||
:max-tag-count="5"
|
||||
:disabled="isReadOnly"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="mb-[8px] font-medium">{{ t('mockManagement.matchRule') }}</div>
|
||||
<div class="mb-[8px] flex items-center justify-between">
|
||||
<a-radio-group v-model:model-value="mockDetail.bodyType" type="button" size="small" :disabled="isReadOnly">
|
||||
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">
|
||||
{{ requestBodyTypeMap[item] }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div
|
||||
v-if="mockDetail.bodyType === RequestBodyFormat.NONE"
|
||||
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
|
||||
>
|
||||
{{ t('apiTestDebug.noneBody') }}
|
||||
</div>
|
||||
<div v-else class="flex h-[calc(100%-34px)]">
|
||||
<MsCodeEditor
|
||||
v-model:model-value="currentBodyCode"
|
||||
:read-only="isReadOnly"
|
||||
class="flex-1"
|
||||
theme="vs"
|
||||
height="100%"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
:show-code-format="true"
|
||||
:language="currentCodeLanguage"
|
||||
>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
|
||||
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
|
||||
import { requestBodyTypeMap } from '@/config/apiTest';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { RequestBodyFormat } from '@/enums/apiEnum';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const isEdit = ref(false);
|
||||
const mockDetail = ref<any>();
|
||||
const isReadOnly = computed(() => !isEdit.value && !mockDetail.value.id);
|
||||
|
||||
// 当前显示的代码
|
||||
const currentBodyCode = computed({
|
||||
get() {
|
||||
if (mockDetail.value.bodyType === RequestBodyFormat.JSON) {
|
||||
return mockDetail.value.jsonBody.jsonValue;
|
||||
}
|
||||
if (mockDetail.value.bodyType === RequestBodyFormat.XML) {
|
||||
return mockDetail.value.xmlBody.value;
|
||||
}
|
||||
return mockDetail.value.rawBody.value;
|
||||
},
|
||||
set(val) {
|
||||
if (mockDetail.value.bodyType === RequestBodyFormat.JSON) {
|
||||
mockDetail.value.jsonBody.jsonValue = val;
|
||||
} else if (mockDetail.value.bodyType === RequestBodyFormat.XML) {
|
||||
mockDetail.value.xmlBody.value = val;
|
||||
} else {
|
||||
mockDetail.value.rawBody.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
// 当前代码编辑器的语言
|
||||
const currentCodeLanguage = computed(() => {
|
||||
if (mockDetail.value.bodyType === RequestBodyFormat.JSON) {
|
||||
return LanguageEnum.JSON;
|
||||
}
|
||||
if (mockDetail.value.bodyType === RequestBodyFormat.XML) {
|
||||
return LanguageEnum.XML;
|
||||
}
|
||||
return LanguageEnum.PLAINTEXT;
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
emit('delete');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -1,7 +1,15 @@
|
|||
<template>
|
||||
<div :class="['p-[16px_22px]', props.class]">
|
||||
<div class="mb-[16px] flex items-center justify-end">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<div :class="['p-[8px_22px]', props.class]">
|
||||
<div :class="['mb-[8px]', 'flex', 'items-center', props.isApi ? 'justify-between' : 'justify-end']">
|
||||
<a-button
|
||||
v-show="props.isApi"
|
||||
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+ADD']"
|
||||
type="primary"
|
||||
@click="createMock"
|
||||
>
|
||||
{{ t('mockManagement.createMock') }}
|
||||
</a-button>
|
||||
<div class="flex gap-[8px]">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiTestManagement.searchPlaceholder')"
|
||||
|
@ -11,6 +19,11 @@
|
|||
@press-enter="loadMockList"
|
||||
@clear="loadMockList"
|
||||
/>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]" @click="loadMockList">
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<ms-base-table
|
||||
|
@ -23,24 +36,45 @@
|
|||
@selected-change="handleTableSelect"
|
||||
@batch-action="handleTableBatch"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<template #num="{ record }">
|
||||
<MsButton type="text" @click="openMockDetailDrawer(record)">
|
||||
{{ record.num }}
|
||||
</MsButton>
|
||||
</template>
|
||||
<template #enable="{ record }">
|
||||
<a-switch
|
||||
v-model="record.enable"
|
||||
size="small"
|
||||
type="line"
|
||||
@change="(value) => changeDefault(value, record)"
|
||||
></a-switch>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<MsButton type="text" @click="debugMock(record)">
|
||||
{{ t('apiTestManagement.debug') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
<MsTableMoreAction :list="tableMoreActionList" @select="handleTableMoreActionSelect($event, record)" />
|
||||
</template>
|
||||
<template v-if="hasAnyPermission(['PROJECT_API_DEFINITION_MOCK:READ+ADD']) && props.isApi" #empty>
|
||||
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
|
||||
{{ t('apiTestManagement.tableNoDataAndPlease') }}
|
||||
<MsButton class="ml-[8px]" @click="createMock">
|
||||
{{ t('mockManagement.createMock') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
</ms-base-table>
|
||||
</div>
|
||||
<mockDetailDrawer v-model:visible="mockDetailDrawerVisible" />
|
||||
<mockDebugDrawer v-model:visible="mockDebugDrawerVisible" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
|
@ -56,12 +90,17 @@
|
|||
import useModal from '@/hooks/useModal';
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
|
||||
import { ApiDefinitionMockDetail } from '@/models/apiTest/management';
|
||||
import { OrdTemplateManagement } from '@/models/setting/template';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
const mockDetailDrawer = defineAsyncComponent(() => import('./mockDetailDrawer.vue'));
|
||||
const mockDebugDrawer = defineAsyncComponent(() => import('./mockDebugDrawer.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
isApi?: boolean; // 接口定义详情的case tab下
|
||||
class?: string;
|
||||
activeModule: string;
|
||||
offspringIds: string[];
|
||||
|
@ -77,8 +116,6 @@
|
|||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const showSubdirectory = ref(false);
|
||||
const checkedEnv = ref('DEV');
|
||||
const keyword = ref('');
|
||||
|
||||
let columns: MsTableColumn = [
|
||||
|
@ -118,6 +155,12 @@
|
|||
showTooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'common.status',
|
||||
dataIndex: 'enable',
|
||||
slotName: 'enable',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'mockManagement.operationUser',
|
||||
slotName: 'createUserName',
|
||||
|
@ -231,6 +274,7 @@
|
|||
Message.success(t('system.orgTemplate.setSuccessfully'));
|
||||
loadMockList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
@ -334,6 +378,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
const mockDetailDrawerVisible = ref(false);
|
||||
const activeMockRecord = ref<ApiDefinitionMockDetail>();
|
||||
|
||||
function createMock() {
|
||||
activeMockRecord.value = undefined;
|
||||
mockDetailDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
function openMockDetailDrawer(record: ApiDefinitionMockDetail) {
|
||||
activeMockRecord.value = record;
|
||||
mockDetailDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
const mockDebugDrawerVisible = ref(false);
|
||||
function debugMock(record: ApiDefinitionMockDetail) {
|
||||
activeMockRecord.value = record;
|
||||
mockDebugDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
loadMockList,
|
||||
});
|
||||
|
|
|
@ -156,7 +156,7 @@
|
|||
:parent-id="nodeData.id"
|
||||
:node-id="nodeData.id"
|
||||
:field-config="{ field: renameFolderTitle }"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:all-names="(nodeData.parent? nodeData.parent.children || [] : folderTree).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:update-module-api="updateModule"
|
||||
:update-api-node-api="updateDefinition"
|
||||
@close="resetFocusNodeKey"
|
||||
|
|
|
@ -123,14 +123,6 @@ export default {
|
|||
'apiTestManagement.collapseApi': 'Hide all requests',
|
||||
'apiTestManagement.paramName': 'Parameter name',
|
||||
'apiTestManagement.paramVal': 'Parameter value',
|
||||
'mockManagement.name': 'Expected name',
|
||||
'mockManagement.apiPath': 'Interface path',
|
||||
'mockManagement.operationUser': 'Operator',
|
||||
'mockManagement.updateTime': 'Update time',
|
||||
'mockManagement.copyMock': 'Copy mock address',
|
||||
'mockManagement.batchEnable': 'Batch enable',
|
||||
'mockManagement.batchDisEnable': 'Batch disable',
|
||||
'mockManagement.batchDeleteMockTip': 'Are you sure you want to delete the selected {count} mocks?',
|
||||
'apiTestManagement.deleteMockTip': 'After deletion, it cannot be restored. Are you sure you want to delete it?',
|
||||
'apiTestManagement.preview': 'Preview',
|
||||
'apiTestManagement.shareUrlCopied': 'Sharing link copied to clipboard',
|
||||
|
@ -214,4 +206,13 @@ export default {
|
|||
'case.detail.dependency.list': 'Reference relationship list',
|
||||
'case.detail.resource.api': 'API',
|
||||
'case.detail.report.delete': 'Report cleared',
|
||||
'mockManagement.name': 'Expected name',
|
||||
'mockManagement.apiPath': 'Interface path',
|
||||
'mockManagement.operationUser': 'Operator',
|
||||
'mockManagement.updateTime': 'Update time',
|
||||
'mockManagement.copyMock': 'Copy mock address',
|
||||
'mockManagement.batchEnable': 'Batch enable',
|
||||
'mockManagement.batchDisEnable': 'Batch disable',
|
||||
'mockManagement.batchDeleteMockTip': 'Are you sure you want to delete the selected {count} mocks?',
|
||||
'mockManagement.allMock': 'All MOCK',
|
||||
};
|
||||
|
|
|
@ -118,14 +118,6 @@ export default {
|
|||
'apiTestManagement.collapseApi': '隐藏全部请求',
|
||||
'apiTestManagement.paramName': '参数名',
|
||||
'apiTestManagement.paramVal': '参数值',
|
||||
'mockManagement.name': '期望名称',
|
||||
'mockManagement.apiPath': '接口路径',
|
||||
'mockManagement.operationUser': '操作人',
|
||||
'mockManagement.updateTime': '更新时间',
|
||||
'mockManagement.copyMock': '复制Mock地址',
|
||||
'mockManagement.batchEnable': '批量启用',
|
||||
'mockManagement.batchDisEnable': '批量禁用',
|
||||
'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个Mock吗?',
|
||||
'apiTestManagement.deleteMockTip': '删除后不可恢复,确认删除吗?',
|
||||
'apiTestManagement.preview': '预览',
|
||||
'apiTestManagement.shareUrlCopied': '分享链接已复制到剪贴板',
|
||||
|
@ -171,7 +163,7 @@ export default {
|
|||
'case.execute.CollectionReport': '集合报告',
|
||||
'case.execute.reportName': '报告名称',
|
||||
'case.execute.pool': '资源池运行',
|
||||
'case.allCase': '全部CASE',
|
||||
'case.allCase': '全部 CASE',
|
||||
'case.detail': '用例详情',
|
||||
'case.caseName': '用例名称',
|
||||
'case.caseNameRequired': '用例名称不能为空',
|
||||
|
@ -208,4 +200,18 @@ export default {
|
|||
'case.detail.dependency.list': '引用关系列表',
|
||||
'case.detail.resource.api': '接口测试',
|
||||
'case.detail.report.delete': '报告已清理',
|
||||
'mockManagement.name': '期望名称',
|
||||
'mockManagement.apiPath': '接口路径',
|
||||
'mockManagement.operationUser': '操作人',
|
||||
'mockManagement.updateTime': '更新时间',
|
||||
'mockManagement.copyMock': '复制Mock地址',
|
||||
'mockManagement.batchEnable': '批量启用',
|
||||
'mockManagement.batchDisEnable': '批量禁用',
|
||||
'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个Mock吗?',
|
||||
'mockManagement.allMock': '全部 MOCK',
|
||||
'mockManagement.createMock': '创建 MOCK',
|
||||
'mockManagement.mockDetail': 'MOCK 详情',
|
||||
'mockManagement.namePlaceholder': '请输入期望名称',
|
||||
'mockManagement.nameNotNull': '期望名称不能为空',
|
||||
'mockManagement.matchRule': '匹配规则',
|
||||
};
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
:parent-id="nodeData.id"
|
||||
:node-id="nodeData.id"
|
||||
:field-config="{ field: renameFolderTitle }"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:all-names="(nodeData.parent? nodeData.parent.children || [] : folderTree).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:update-module-api="updateModule"
|
||||
@close="resetFocusNodeKey"
|
||||
@rename-finish="initModules"
|
||||
|
@ -260,6 +260,7 @@
|
|||
};
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue