feat(接口测试): pdf展示&部分 mock 页面

This commit is contained in:
baiqi 2024-04-29 19:07:44 +08:00 committed by 刘瑞斌
parent 10e3a7af03
commit 03bbdbf15e
22 changed files with 431 additions and 69 deletions

View File

@ -1,7 +1,7 @@
@font-face { @font-face {
font-family: iconfont; /* Project id 3462279 */ font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1711511079663') format('woff2'), url('iconfont.woff?t=1711511079663') format('woff'), src: url('iconfont.woff2?t=1714372635707') format('woff2'), url('iconfont.woff?t=1714372635707') format('woff'),
url('iconfont.ttf?t=1711511079663') format('truetype'), url('iconfont.svg?t=1711511079663#iconfont') format('svg'); url('iconfont.ttf?t=1714372635707') format('truetype'), url('iconfont.svg?t=1714372635707#iconfont') format('svg');
} }
.iconfont { .iconfont {
font-size: 16px; font-size: 16px;
@ -10,6 +10,12 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-icon_menu_unfold::before {
content: '\e7a9';
}
.icon-icon_menu_fold::before {
content: '\e7aa';
}
.icon-icon_stop::before { .icon-icon_stop::before {
content: '\e7a8'; content: '\e7a8';
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,20 @@
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"description": "DE、MS项目icon管理", "description": "DE、MS项目icon管理",
"glyphs": [ "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", "icon_id": "39710057",
"name": "icon_stop", "name": "icon_stop",

View File

@ -14,6 +14,10 @@
/> />
<missing-glyph /> <missing-glyph />
<glyph glyph-name="icon_menu_unfold" unicode="&#59305;" 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="&#59306;" 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="&#59304;" 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_stop" unicode="&#59304;" 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="&#59303;" 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" /> <glyph glyph-name="icon_env1" unicode="&#59303;" 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

View File

@ -20,7 +20,7 @@
import { getFirstRouterNameByCurrentRoute } from '@/utils/permission'; import { getFirstRouterNameByCurrentRoute } from '@/utils/permission';
import { listenerRouteChange } from '@/utils/route-listener'; 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 useMenuTree from './use-menu-tree';
import type { RouteMeta } from 'vue-router'; import type { RouteMeta } from 'vue-router';
@ -123,6 +123,11 @@
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]]; selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]];
} }
if (newRoute.fullPath.includes(RouteEnum.SETTING)) {
appStore.updateSettings({ menuCollapse: false });
} else {
appStore.updateSettings({ menuCollapse: true });
}
}, true); }, true);
const setCollapse = (val: boolean) => { const setCollapse = (val: boolean) => {
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val }); if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });

View File

@ -195,7 +195,7 @@
class="no-content relative mt-[8px] border-b" class="no-content relative mt-[8px] border-b"
/> />
</div> </div>
<div ref="splitContainerRef" class="request-and-response h-[calc(100%-100px)]"> <div ref="splitContainerRef" class="request-and-response h-[calc(100%-92px)]">
<MsSplitBox <MsSplitBox
ref="verticalSplitBoxRef" ref="verticalSplitBoxRef"
v-model:size="splitBoxSize" v-model:size="splitBoxSize"
@ -991,8 +991,12 @@
const saveModalFormRef = ref<FormInstance>(); const saveModalFormRef = ref<FormInstance>();
const saveLoading = ref(false); const saveLoading = ref(false);
const selectTree = computed(() => { const selectTree = computed(() => {
if (saveModalVisible.value || (!props.isCase && props.isDefinition && saveModalVisible.value)) { if (
// TODO:worker线 requestVModel.value.activeTab === RequestComposition.BASE_INFO ||
saveModalVisible.value ||
(!props.isCase && props.isDefinition && saveModalVisible.value)
) {
// tab TODO:worker线
return filterTree(cloneDeep(props.moduleTree || []), (e) => { return filterTree(cloneDeep(props.moduleTree || []), (e) => {
e.draggable = false; e.draggable = false;
return e.type === 'MODULE'; return e.type === 'MODULE';

View File

@ -222,7 +222,6 @@
import { defaultKeyValueParamItem, defaultResponseItem, statusCodes } from '../../config'; import { defaultKeyValueParamItem, defaultResponseItem, statusCodes } from '../../config';
const props = defineProps<{ const props = defineProps<{
responseDefinition: ResponseDefinition[];
uploadTempFileApi?: (file: File) => Promise<any>; // uploadTempFileApi?: (file: File) => Promise<any>; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -240,15 +239,25 @@
const responseTabs = defineModel<ResponseItem[]>('responseDefinition', { const responseTabs = defineModel<ResponseItem[]>('responseDefinition', {
required: true, 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>) { function addResponseTab(defaultProps?: Partial<ResponseItem>) {
const id = new Date().getTime();
responseTabs.value.push({ responseTabs.value.push({
...cloneDeep(defaultResponseItem), ...cloneDeep(defaultResponseItem),
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }), label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
name: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }), name: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
...defaultProps, ...defaultProps,
id: new Date().getTime(), id,
defaultFlag: false, defaultFlag: false,
showPopConfirm: false, showPopConfirm: false,
showRenamePopConfirm: false, showRenamePopConfirm: false,

View File

@ -3,7 +3,7 @@
<div :class="['response-head', props.isExpanded ? '' : 'border-t']"> <div :class="['response-head', props.isExpanded ? '' : 'border-t']">
<slot name="titleLeft"> <slot name="titleLeft">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<template v-if="props.activeLayout === 'vertical'"> <template v-if="activeLayout === 'vertical'">
<MsButton <MsButton
v-if="props.isExpanded" v-if="props.isExpanded"
type="icon" type="icon"
@ -44,7 +44,7 @@
<div v-else class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div> <div v-else class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
<a-radio-group <a-radio-group
v-if="!props.hideLayoutSwitch" v-if="!props.hideLayoutSwitch"
v-model:model-value="innerLayout" v-model:model-value="activeLayout"
type="button" type="button"
size="small" size="small"
@change="(val) => emit('changeLayout', val as Direction)" @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]']" :class="[isResponseModel ? 'h-[381px] w-full' : 'h-[calc(100%-35px)] w-full px-[16px] pb-[16px]']"
> >
<edit <edit
v-if="props.isEdit && activeResponseType === 'content' && innerResponseDefinition" v-if="props.isEdit && activeResponseType === 'content' && responseDefinition"
v-model:response-definition="innerResponseDefinition" v-model:response-definition="responseDefinition"
:upload-temp-file-api="props.uploadTempFileApi" :upload-temp-file-api="props.uploadTempFileApi"
@change="handleResponseChange" @change="handleResponseChange"
/> />
<result <result
v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')" v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
v-model:active-tab="innerActiveTab" v-model:active-tab="activeTab"
:request-result="props.requestResult" :request-result="props.requestResult"
:console="props.console" :console="props.console"
:is-http-protocol="props.isHttpProtocol" :is-http-protocol="props.isHttpProtocol"
@ -97,13 +97,10 @@
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
activeTab: ResponseComposition;
isExpanded?: boolean; isExpanded?: boolean;
isPriorityLocalExec?: boolean; isPriorityLocalExec?: boolean;
requestUrl?: string; requestUrl?: string;
isHttpProtocol?: boolean; isHttpProtocol?: boolean;
activeLayout?: Direction;
responseDefinition?: ResponseItem[];
requestResult?: RequestResult; requestResult?: RequestResult;
console?: string; console?: string;
hideLayoutSwitch?: boolean; // hideLayoutSwitch?: boolean; //
@ -117,7 +114,6 @@
}>(), }>(),
{ {
isExpanded: true, isExpanded: true,
activeLayout: 'vertical',
hideLayoutSwitch: false, hideLayoutSwitch: false,
showEmpty: true, showEmpty: true,
} }
@ -131,21 +127,21 @@
const { t } = useI18n(); const { t } = useI18n();
const innerLayout = defineModel<Direction>('activeLayout', { const activeLayout = defineModel<Direction>('activeLayout', {
default: 'vertical', default: 'vertical',
}); });
const innerActiveTab = defineModel<ResponseComposition>('activeTab', { const activeTab = defineModel<ResponseComposition>('activeTab', {
required: true, required: true,
}); });
const innerResponseDefinition = defineModel<ResponseItem[]>('responseDefinition', { const responseDefinition = defineModel<ResponseItem[]>('responseDefinition', {
default: [], default: [],
}); });
watchEffect(() => { watchEffect(() => {
// null // null
let hasInvalid = false; let hasInvalid = false;
let validResponseDefinition: ResponseItem[] = []; let validResponseDefinition: ResponseItem[] = [];
if (props.responseDefinition && props.responseDefinition.length > 0) { if (responseDefinition.value.length > 0) {
validResponseDefinition = props.responseDefinition.map((item, i) => { validResponseDefinition = responseDefinition.value.map((item, i) => {
// null // null
if (!item.headers) { if (!item.headers) {
item.headers = []; item.headers = [];
@ -189,7 +185,7 @@
}); });
} }
if (hasInvalid) { if (hasInvalid) {
innerResponseDefinition.value = validResponseDefinition; responseDefinition.value = validResponseDefinition;
} }
}); });

View File

@ -1,18 +1,26 @@
<template> <template>
<div v-if="showImg"> <div v-if="showImg || isPdf" :class="showType === 'text' ? '' : 'h-full'">
<div class="mb-[8px] flex items-center gap-[16px]"> <div class="mb-[8px] flex items-center gap-[16px]">
<a-button type="outline" class="arco-btn-outline--secondary" size="mini" @click="handleDownload"> <a-button type="outline" class="arco-btn-outline--secondary" size="mini" @click="handleDownload">
{{ t('common.download') }} {{ t('common.download') }}
</a-button> </a-button>
<a-radio-group v-model:model-value="showType" type="button" size="small"> <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 value="text">{{ t('common.text') }}</a-radio>
</a-radio-group> </a-radio-group>
</div> </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> </div>
<MsCodeEditor <MsCodeEditor
v-show="!showImg || showType === 'text'" v-show="(!showImg && !isPdf) || showType === 'text'"
ref="responseEditorRef" ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body || ''" :model-value="props.requestResult?.responseResult.body || ''"
:language="responseLanguage" :language="responseLanguage"
@ -81,6 +89,12 @@
} }
return false; return false;
}); });
const isPdf = computed(() => {
if (props.requestResult) {
return props.requestResult.responseResult.contentType === 'application/pdf';
}
return false;
});
const imageUrl = computed(() => { const imageUrl = computed(() => {
if (props.requestResult) { if (props.requestResult) {
return `data:${props.requestResult?.responseResult.contentType};base64,${props.requestResult?.responseResult.imageUrl}`; return `data:${props.requestResult?.responseResult.contentType};base64,${props.requestResult?.responseResult.imageUrl}`;
@ -88,10 +102,24 @@
return ''; 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() { 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]}`); downloadUrlFile(imageUrl.value, `response.${props.requestResult?.responseResult.contentType.split('/')[1]}`);
} }
} }

View File

@ -105,7 +105,19 @@
:member-options="memberOptions" :member-options="memberOptions"
/> />
</a-tab-pane> </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> </a-tabs>
</div> </div>
</div> </div>
@ -115,7 +127,6 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; 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 MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import apiTable from './apiTable.vue'; import apiTable from './apiTable.vue';
import executeButton from '@/views/api-test/components/executeButton.vue'; import executeButton from '@/views/api-test/components/executeButton.vue';
@ -156,6 +167,8 @@
() => import('@/views/api-test/components/requestComposition/index.vue') () => import('@/views/api-test/components/requestComposition/index.vue')
); );
const preview = defineAsyncComponent(() => import('./preview/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<{ const props = defineProps<{
activeModule: string; activeModule: string;

View File

@ -47,9 +47,9 @@
</div> </div>
</template> </template>
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">{{ <MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
record.num {{ record.num }}
}}</MsButton> </MsButton>
</template> </template>
<template #caseLevel="{ record }"> <template #caseLevel="{ record }">
<a-select <a-select

View File

@ -59,6 +59,12 @@
:member-options="memberOptions" :member-options="memberOptions"
@delete-case="(id) => handleDeleteApiFromModuleTree(id)" @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> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -71,7 +77,6 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.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 { getProtocolList } from '@/api/modules/api-test/common';
import { getProjectOptions } from '@/api/modules/project-management/projectMember'; import { getProjectOptions } from '@/api/modules/project-management/projectMember';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -91,6 +96,10 @@
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config'; 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<{ const props = defineProps<{
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
@ -113,6 +122,7 @@
const tabOptions = [ const tabOptions = [
{ label: 'API', value: 'api' }, { label: 'API', value: 'api' },
...(hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ']) ? [{ label: 'CASE', value: 'case' }] : []), ...(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>>(); const apiRef = ref<InstanceType<typeof api>>();
@ -229,7 +239,13 @@
// //
function currentTabChange(val: any) { 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(); changeActiveApiTabToFirst();
} }

View File

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

View File

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

View File

@ -1,7 +1,15 @@
<template> <template>
<div :class="['p-[16px_22px]', props.class]"> <div :class="['p-[8px_22px]', props.class]">
<div class="mb-[16px] flex items-center justify-end"> <div :class="['mb-[8px]', 'flex', 'items-center', props.isApi ? 'justify-between' : 'justify-end']">
<div class="flex items-center gap-[8px]"> <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 <a-input-search
v-model:model-value="keyword" v-model:model-value="keyword"
:placeholder="t('apiTestManagement.searchPlaceholder')" :placeholder="t('apiTestManagement.searchPlaceholder')"
@ -11,6 +19,11 @@
@press-enter="loadMockList" @press-enter="loadMockList"
@clear="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>
</div> </div>
<ms-base-table <ms-base-table
@ -23,24 +36,45 @@
@selected-change="handleTableSelect" @selected-change="handleTableSelect"
@batch-action="handleTableBatch" @batch-action="handleTableBatch"
> >
<template #action="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="openMockDetailDrawer(record)">
{{ record.num }}
</MsButton>
</template>
<template #enable="{ record }">
<a-switch <a-switch
v-model="record.enable" v-model="record.enable"
size="small" size="small"
type="line" type="line"
@change="(value) => changeDefault(value, record)" @change="(value) => changeDefault(value, record)"
></a-switch> ></a-switch>
</template>
<template #action="{ record }">
<MsButton type="text" @click="debugMock(record)">
{{ t('apiTestManagement.debug') }}
</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="tableMoreActionList" @select="handleTableMoreActionSelect($event, record)" /> <MsTableMoreAction :list="tableMoreActionList" @select="handleTableMoreActionSelect($event, record)" />
</template> </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> </ms-base-table>
</div> </div>
<mockDetailDrawer v-model:visible="mockDetailDrawerVisible" />
<mockDebugDrawer v-model:visible="mockDebugDrawerVisible" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
@ -56,12 +90,17 @@
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import { ApiDefinitionMockDetail } from '@/models/apiTest/management'; import { ApiDefinitionMockDetail } from '@/models/apiTest/management';
import { OrdTemplateManagement } from '@/models/setting/template'; import { OrdTemplateManagement } from '@/models/setting/template';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const mockDetailDrawer = defineAsyncComponent(() => import('./mockDetailDrawer.vue'));
const mockDebugDrawer = defineAsyncComponent(() => import('./mockDebugDrawer.vue'));
const props = defineProps<{ const props = defineProps<{
isApi?: boolean; // case tab
class?: string; class?: string;
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
@ -77,8 +116,6 @@
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const showSubdirectory = ref(false);
const checkedEnv = ref('DEV');
const keyword = ref(''); const keyword = ref('');
let columns: MsTableColumn = [ let columns: MsTableColumn = [
@ -118,6 +155,12 @@
showTooltip: true, showTooltip: true,
width: 200, width: 200,
}, },
{
title: 'common.status',
dataIndex: 'enable',
slotName: 'enable',
width: 100,
},
{ {
title: 'mockManagement.operationUser', title: 'mockManagement.operationUser',
slotName: 'createUserName', slotName: 'createUserName',
@ -231,6 +274,7 @@
Message.success(t('system.orgTemplate.setSuccessfully')); Message.success(t('system.orgTemplate.setSuccessfully'));
loadMockList(); loadMockList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); 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({ defineExpose({
loadMockList, loadMockList,
}); });

View File

@ -156,7 +156,7 @@
:parent-id="nodeData.id" :parent-id="nodeData.id"
:node-id="nodeData.id" :node-id="nodeData.id"
:field-config="{ field: renameFolderTitle }" :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-module-api="updateModule"
:update-api-node-api="updateDefinition" :update-api-node-api="updateDefinition"
@close="resetFocusNodeKey" @close="resetFocusNodeKey"

View File

@ -123,14 +123,6 @@ export default {
'apiTestManagement.collapseApi': 'Hide all requests', 'apiTestManagement.collapseApi': 'Hide all requests',
'apiTestManagement.paramName': 'Parameter name', 'apiTestManagement.paramName': 'Parameter name',
'apiTestManagement.paramVal': 'Parameter value', '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.deleteMockTip': 'After deletion, it cannot be restored. Are you sure you want to delete it?',
'apiTestManagement.preview': 'Preview', 'apiTestManagement.preview': 'Preview',
'apiTestManagement.shareUrlCopied': 'Sharing link copied to clipboard', 'apiTestManagement.shareUrlCopied': 'Sharing link copied to clipboard',
@ -214,4 +206,13 @@ export default {
'case.detail.dependency.list': 'Reference relationship list', 'case.detail.dependency.list': 'Reference relationship list',
'case.detail.resource.api': 'API', 'case.detail.resource.api': 'API',
'case.detail.report.delete': 'Report cleared', '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',
}; };

View File

@ -118,14 +118,6 @@ export default {
'apiTestManagement.collapseApi': '隐藏全部请求', 'apiTestManagement.collapseApi': '隐藏全部请求',
'apiTestManagement.paramName': '参数名', 'apiTestManagement.paramName': '参数名',
'apiTestManagement.paramVal': '参数值', 'apiTestManagement.paramVal': '参数值',
'mockManagement.name': '期望名称',
'mockManagement.apiPath': '接口路径',
'mockManagement.operationUser': '操作人',
'mockManagement.updateTime': '更新时间',
'mockManagement.copyMock': '复制Mock地址',
'mockManagement.batchEnable': '批量启用',
'mockManagement.batchDisEnable': '批量禁用',
'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个Mock吗',
'apiTestManagement.deleteMockTip': '删除后不可恢复,确认删除吗?', 'apiTestManagement.deleteMockTip': '删除后不可恢复,确认删除吗?',
'apiTestManagement.preview': '预览', 'apiTestManagement.preview': '预览',
'apiTestManagement.shareUrlCopied': '分享链接已复制到剪贴板', 'apiTestManagement.shareUrlCopied': '分享链接已复制到剪贴板',
@ -171,7 +163,7 @@ export default {
'case.execute.CollectionReport': '集合报告', 'case.execute.CollectionReport': '集合报告',
'case.execute.reportName': '报告名称', 'case.execute.reportName': '报告名称',
'case.execute.pool': '资源池运行', 'case.execute.pool': '资源池运行',
'case.allCase': '全部CASE', 'case.allCase': '全部 CASE',
'case.detail': '用例详情', 'case.detail': '用例详情',
'case.caseName': '用例名称', 'case.caseName': '用例名称',
'case.caseNameRequired': '用例名称不能为空', 'case.caseNameRequired': '用例名称不能为空',
@ -208,4 +200,18 @@ export default {
'case.detail.dependency.list': '引用关系列表', 'case.detail.dependency.list': '引用关系列表',
'case.detail.resource.api': '接口测试', 'case.detail.resource.api': '接口测试',
'case.detail.report.delete': '报告已清理', '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': '匹配规则',
}; };

View File

@ -110,7 +110,7 @@
:parent-id="nodeData.id" :parent-id="nodeData.id"
:node-id="nodeData.id" :node-id="nodeData.id"
:field-config="{ field: renameFolderTitle }" :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-module-api="updateModule"
@close="resetFocusNodeKey" @close="resetFocusNodeKey"
@rename-finish="initModules" @rename-finish="initModules"
@ -260,6 +260,7 @@
}; };
}); });
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} }
} }