feat(接口场景): 场景步骤查看步骤详情

This commit is contained in:
baiqi 2024-03-25 14:46:37 +08:00 committed by Craftsman
parent d608df0e93
commit adb640ec13
8 changed files with 132 additions and 92 deletions

View File

@ -83,6 +83,6 @@ export const apiSocket = (url: string, host?: string) => {
return new WebSocket(uri);
};
export function getSocket(reportId: string, socketUrl?: string, host?: string) {
export function getSocket(reportId: string | number, socketUrl?: string, host?: string) {
return apiSocket(`${socketUrl || ConnectionWebsocketUrl}/${reportId}`, host);
}

View File

@ -263,7 +263,7 @@
border: 1px solid var(--color-text-input-border);
background-color: var(--color-text-fff);
&:not(:disabled):hover {
border-color: rgb(var(--primary-5));
border-color: rgb(var(--primary-5)) !important;
background-color: white;
}
input::placeholder {
@ -363,8 +363,20 @@
background: none;
box-shadow: none;
}
.arco-form-item-status-success .arco-input-wrapper:not(.arco-input-disabled) {
border-color: var(--color-text-input-border);
.arco-form-item-status-success {
&:hover {
.arco-input-wrapper:not(.arco-input-disabled),
.arco-select-view:not(.arco-select-view-disabled),
.arco-textarea-wrapper:not(.arco-textarea-disabled) {
background: white;
}
}
.arco-input-wrapper:not(.arco-input-disabled),
.arco-select-view:not(.arco-select-view-disabled),
.arco-textarea-wrapper:not(.arco-textarea-disabled) {
border-color: var(--color-text-input-border);
background: white;
}
}
.arco-form-item-message {
width: 100%;

View File

@ -331,6 +331,7 @@ export interface ScenarioStepItem {
projectId?: string;
versionId?: string;
children?: ScenarioStepItem[];
isNew: boolean; // 是否新建的步骤,引用复制类型以此区分调用步骤详情还是资源详情
// 页面渲染以及交互需要字段
checked?: boolean; // 是否选中
expanded?: boolean; // 是否展开

View File

@ -283,7 +283,6 @@
import { cloneDeep, debounce } from 'lodash-es';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
@ -322,7 +321,6 @@
RequestConditionProcessor,
RequestMethods,
ResponseComposition,
ScenarioStepRefType,
ScenarioStepType,
} from '@/enums/apiEnum';
@ -349,20 +347,25 @@
export interface RequestCustomAttr {
type: 'api';
name: string;
stepId: string | number; // id
resourceId: string | number; // id
isNew: boolean;
protocol: string;
activeTab: RequestComposition;
executeLoading: boolean; // loading
isCopy?: boolean; //
isExecute?: boolean; //
responseActiveTab: ResponseComposition;
unSaved: boolean;
uploadFileIds: string[];
linkFileIds: string[];
}
export type RequestParam = ExecuteApiRequestFullParams & {
response?: RequestTaskResult;
customizeRequestEnvEnable: boolean;
request?: ExecuteApiRequestFullParams; //
} & RequestCustomAttr &
TabItem;
} & RequestCustomAttr;
const props = defineProps<{
request?: RequestParam; //
@ -398,8 +401,10 @@
const loading = defineModel<boolean>('detailLoading', { default: false });
const defaultDebugParams: RequestParam = {
name: '',
type: 'api',
id: '',
stepId: '',
resourceId: '',
customizeRequestEnvEnable: false,
protocol: 'HTTP',
url: '',
@ -607,7 +612,8 @@
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
);
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request?.request === null);
// api props.request
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined);
const isEditableApi = computed(
() => _stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step
);
@ -616,7 +622,7 @@
const handlePluginFormChange = debounce(() => {
if (isEditableApi.value) {
//
temporaryPluginFormMap[requestVModel.value.id] = fApi.value?.formData();
temporaryPluginFormMap[requestVModel.value.stepId] = fApi.value?.formData();
}
handleActiveDebugChange();
}, 300);
@ -636,7 +642,6 @@
if (currentFormFields && currentFormFields.length < fields.length) {
fApi.value?.hidden(false, fields);
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
fApi.value?.refresh();
} else {
//
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
@ -648,27 +653,30 @@
* 设置插件表单数据
*/
function setPluginFormData() {
const tempForm = temporaryPluginFormMap[requestVModel.value.id];
const tempForm = temporaryPluginFormMap[requestVModel.value.stepId];
console.log('setPluginFormData', temporaryPluginFormMap, requestVModel.value.stepId);
if (tempForm || !requestVModel.value.isNew) {
//
const formData = isEditableApi.value ? tempForm || requestVModel.value : requestVModel.value;
if (fApi.value) {
fApi.value.nextRefresh(() => {
const form = {};
controlPluginFormFields().forEach((key) => {
form[key] = formData[key];
nextTick(() => {
if (fApi.value) {
fApi.value.nextRefresh(() => {
const form = {};
controlPluginFormFields().forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(cloneDeep(form));
setTimeout(() => {
// 300ms handlePluginFormChange
isInitPluginForm.value = true;
}, 300);
});
fApi.value?.setValue(cloneDeep(form));
fApi.value?.clearValidateState();
setTimeout(() => {
// 300ms handlePluginFormChange
isInitPluginForm.value = true;
}, 300);
});
}
}
});
} else {
fApi.value?.nextTick(() => {
nextTick(() => {
controlPluginFormFields();
fApi.value?.clearValidateState();
fApi.value?.resetFields();
isInitPluginForm.value = true;
});
@ -831,7 +839,6 @@
return conditionCopy;
}
const reportId = ref('');
const websocket = ref<WebSocket>();
/**
@ -839,14 +846,14 @@
*/
function debugSocket(executeType?: 'localExec' | 'serverExec') {
websocket.value = getSocket(
reportId.value,
requestVModel.value.stepId,
executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? localExecuteUrl.value : ''
);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (requestVModel.value.reportId === data.reportId) {
if (requestVModel.value.stepId === data.reportId) {
// tabtab
requestVModel.value.response = data.taskResult;
requestVModel.value.executeLoading = false;
@ -922,7 +929,8 @@
}
return {
...requestParams,
id: requestVModel.value.id,
resourceId: requestVModel.value.resourceId,
stepId: requestVModel.value.stepId,
activeTab: requestVModel.value.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
responseActiveTab: ResponseComposition.BODY,
protocol: requestVModel.value.protocol,
@ -1016,7 +1024,7 @@
async function initQuoteApiDetail() {
try {
loading.value = true;
const res = await getDefinitionDetail(requestVModel.value.id);
const res = await getDefinitionDetail(props.step?.resourceId || '');
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
@ -1032,9 +1040,10 @@
response: cloneDeep(defaultResponse),
url: res.path,
name: res.name, // requestnamenull
id: res.id,
resourceId: res.id,
...parseRequestBodyResult,
};
console.log('initQuoteApiDetail', requestVModel.value);
nextTick(() => {
// loading
loading.value = false;
@ -1054,53 +1063,26 @@
await initProtocolList();
}
if (props.request) {
// api api
requestVModel.value = cloneDeep({
...defaultDebugParams,
...props.request,
isNew: false,
});
if (
_stepType.value.isQuoteApi ||
isCopyApiNeedInit.value
// (request.requestrequest null)
) {
if (_stepType.value.isQuoteApi) {
//
await initQuoteApiDetail();
}
if (
props.step?.stepType === ScenarioStepType.API &&
props.step?.refType === ScenarioStepRefType.REF &&
props.request.request &&
requestVModel.value.request
) {
// queryrest
['headers', 'query', 'rest'].forEach((type) => {
props.request?.request?.[type]?.forEach((item) => {
const index = requestVModel.value.request?.[type]?.findIndex((itemReq) => itemReq.key === item.key);
if (index > -1 && requestVModel.value.request) {
requestVModel.value.request[type][index].value = item.value;
requestVModel.value[type] = requestVModel.value.request?.[type];
}
});
});
if (props.request.request.body.bodyType !== 'NONE') {
['formDataBody', 'wwwFormBody'].forEach((type) => {
props.request?.request?.body[type].formValues.forEach((item) => {
const index = requestVModel.value.request?.body[type].formValues.findIndex(
(itemReq) => itemReq.key === item.key
);
if (index > -1 && requestVModel.value.request?.body) {
requestVModel.value.request.body[type].formValues[index].value = item.value;
requestVModel.value.body = requestVModel.value.request?.body;
}
});
});
}
}
handleActiveDebugProtocolChange(requestVModel.value.protocol);
} else if (_stepType.value.isQuoteApi || isCopyApiNeedInit.value) {
// api props.request
await initQuoteApiDetail();
handleActiveDebugProtocolChange(requestVModel.value.protocol);
} else {
//
requestVModel.value = cloneDeep({
...defaultDebugParams,
id: getGenerateId(),
stepId: getGenerateId(),
});
}
}

View File

@ -121,7 +121,6 @@ export default function useCreateActions() {
}
return {
...cloneDeep(defaultStepItemCommon),
...item,
id,
config: {
...defaultStepItemCommon.config,

View File

@ -85,7 +85,7 @@
@change="handleStepContentChange($event, step)"
@click.stop
/>
<!-- APICASE场景步骤名称 -->
<!-- 自定义请求APICASE场景步骤名称 -->
<template v-if="checkStepIsApi(step)">
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.config.method" />
<div
@ -329,6 +329,8 @@
return quoteContent;
}
switch (step.stepType) {
case ScenarioStepType.CUSTOM_REQUEST:
return quoteContent;
case ScenarioStepType.LOOP_CONTROLLER:
return loopControlContent;
case ScenarioStepType.IF_CONTROLLER:
@ -595,8 +597,8 @@
if (_stepType.isCopyApi || _stepType.isQuoteApi || step.stepType === ScenarioStepType.CUSTOM_REQUEST) {
// api api api
activeStep.value = step;
if (stepDetails.value[step.id] === undefined) {
// api api
if (stepDetails.value[step.id] === undefined && !step.isNew) {
//
await getStepDetail(step);
}
customApiDrawerVisible.value = true;
@ -700,14 +702,14 @@
*/
function addCustomApiStep(request: RequestParam) {
request.isNew = false;
stepDetails.value[request.id] = request;
stepDetails.value[request.stepId] = request;
if (activeStep.value && activeCreateAction.value) {
handleCreateStep(
{
stepType: ScenarioStepType.CUSTOM_REQUEST,
name: t('apiScenario.customApi'),
method: request.method,
id: request.id,
id: request.stepId,
projectId: appStore.currentProjectId,
},
activeStep.value,
@ -724,7 +726,7 @@
protocol: request.protocol,
method: request.method,
},
id: request.id,
id: request.stepId,
sort: steps.value.length + 1,
stepType: ScenarioStepType.CUSTOM_REQUEST,
refType: ScenarioStepRefType.DIRECT,
@ -732,6 +734,7 @@
projectId: appStore.currentProjectId,
});
}
console.log(steps.value);
}
/**

View File

@ -1,5 +1,5 @@
<template>
<MsSplitBox :size="0.7" :max="0.9" :min="0.7" direction="horizontal" expand-direction="right">
<MsSplitBox ref="splitBoxRef" :size="0.7" :max="0.9" :min="0.7" direction="horizontal" expand-direction="right">
<template #first>
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="p-[16px]">
@ -36,7 +36,7 @@
<div class="p-[16px]">
<!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<a-form ref="activeApiTabFormRef" :model="scenario" layout="vertical">
<a-form ref="createFormRef" :model="scenario" layout="vertical">
<a-form-item
field="name"
:label="t('apiScenario.name')"
@ -141,6 +141,8 @@
</template>
<script setup lang="ts">
import { FormInstance } from '@arco-design/web-vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
@ -172,6 +174,23 @@
const scenario = defineModel<Scenario>('scenario', {
required: true,
});
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const createFormRef = ref<FormInstance>();
function validScenarioForm(cb: () => Promise<void>) {
createFormRef.value?.validate(async (errors) => {
if (errors) {
splitBoxRef.value?.expand();
} else {
cb();
}
});
}
defineExpose({
validScenarioForm,
});
</script>
<style lang="less" scoped>

View File

@ -3,14 +3,14 @@
<div class="flex items-center justify-between p-[24px_24px_8px_24px]">
<MsEditableTab
v-model:active-tab="activeScenarioTab"
v-model:tabs="apiTabs"
v-model:tabs="scenarioTabs"
class="flex-1 overflow-hidden"
@add="() => newTab()"
>
<template #label="{ tab }">
<a-tooltip :content="tab.label" :mouse-enter-delay="500">
<a-tooltip :content="tab.name || tab.label" :mouse-enter-delay="500">
<div class="one-line-text max-w-[144px]">
{{ tab.label }}
{{ tab.name || tab.label }}
</div>
</a-tooltip>
</template>
@ -61,7 +61,7 @@
</MsSplitBox>
</div>
<div v-else-if="activeScenarioTab.isNew" class="pageWrap">
<create v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create>
<create ref="createRef" v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create>
</div>
<div v-else class="pageWrap">
<detail v-model:scenario="activeScenarioTab"></detail>
@ -107,33 +107,33 @@
const { t } = useI18n();
const apiTabs = ref<ScenarioParams[]>([
const scenarioTabs = ref<ScenarioParams[]>([
{
id: 'all',
label: t('apiScenario.allScenario'),
closable: false,
} as ScenarioParams,
]);
const activeScenarioTab = ref<ScenarioParams>(apiTabs.value[0] as ScenarioParams);
const activeScenarioTab = ref<ScenarioParams>(scenarioTabs.value[0] as ScenarioParams);
function newTab(defaultScenarioInfo?: Scenario, isCopy = false) {
if (defaultScenarioInfo) {
apiTabs.value.push({
scenarioTabs.value.push({
...defaultScenarioInfo,
id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
isNew: false,
});
} else {
apiTabs.value.push({
scenarioTabs.value.push({
...cloneDeep(defaultScenario),
id: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
label: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
id: getGenerateId(),
label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`,
moduleId: 'root',
priority: 'P0',
});
}
activeScenarioTab.value = apiTabs.value[apiTabs.value.length - 1] as ScenarioParams;
activeScenarioTab.value = scenarioTabs.value[scenarioTabs.value.length - 1] as ScenarioParams;
}
const folderTree = ref<ModuleTreeNode[]>([]);
@ -182,9 +182,10 @@
onBeforeMount(selectRecycleCount);
const createRef = ref<InstanceType<typeof create>>();
const saveLoading = ref(false);
async function saveScenario() {
async function realSaveScenario() {
try {
saveLoading.value = true;
if (activeScenarioTab.value.isNew) {
@ -195,7 +196,19 @@
const scenarioDetail = await getScenarioDetail(res.id);
scenarioDetail.stepDetails = {};
scenarioDetail.isNew = false;
activeScenarioTab.value = scenarioDetail as ScenarioParams;
scenarioDetail.id = res.id;
if (!scenarioDetail.steps) {
scenarioDetail.steps = [];
}
const index = scenarioTabs.value.findIndex((e) => e.id === activeScenarioTab.value.id);
if (index !== -1) {
const newScenarioTab = {
...cloneDeep(activeScenarioTab.value),
...scenarioDetail,
};
scenarioTabs.value.splice(index, 1, newScenarioTab);
activeScenarioTab.value = newScenarioTab;
}
} else {
await updateScenario({
...activeScenarioTab.value,
@ -212,11 +225,22 @@
}
}
function saveScenario() {
if (activeScenarioTab.value.isNew) {
createRef.value?.validScenarioForm(realSaveScenario);
} else {
realSaveScenario();
}
}
async function openScenarioTab(record: ApiScenarioTableItem, isCopy?: boolean) {
try {
appStore.showLoading();
const res = await getScenarioDetail(record.id);
res.stepDetails = {};
if (!res.steps) {
res.steps = [];
}
newTab(res, isCopy);
} catch (error) {
// eslint-disable-next-line no-console