fix(all): 修复部分 bug

This commit is contained in:
baiqi 2024-04-07 18:38:19 +08:00 committed by 刘瑞斌
parent c7bc5ed7f5
commit 41ccb313aa
41 changed files with 641 additions and 366 deletions

View File

@ -6,7 +6,6 @@
</template>
<script lang="ts" setup>
import { computed, onBeforeMount, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useEventListener, useWindowSize } from '@vueuse/core';
@ -75,7 +74,7 @@
const checkIsLogin = async () => {
const isLogin = await userStore.isLogin();
const isLoginPage = route.name === 'login';
if (isLogin && appStore.currentProjectId && appStore.currentProjectId !== 'no_such_project') {
if (isLogin && appStore.currentProjectId !== 'no_such_project') {
//
try {
const HasProjectPermission = await getUserHasProjectPermission(appStore.currentProjectId);

View File

@ -387,6 +387,7 @@
}
}
.arco-form-item-message {
margin-bottom: 16px; // 设计要求表单输入行报错信息距离下一个表单输入行间距为16px
width: 100%;
}
.hidden-item {

View File

@ -47,14 +47,24 @@
</div>
<div class="px-[16px]">
<div class="api-item-label">{{ t('ms.personal.desc') }}</div>
<a-textarea
v-if="item.showDescInput"
v-model:model-value="item.description"
:placeholder="t('common.pleaseInput')"
:max-length="500"
@blur="handleDescChange(item)"
></a-textarea>
<div v-else class="desc-line api-item-value">
<a-tooltip :content="item.description" :disabled="!item.description">
<div class="api-item-value one-line-text">{{ item.description || '-' }}</div>
<div class="one-line-text">{{ item.description || '-' }}</div>
</a-tooltip>
<MsIcon type="icon-icon_edit_outlined" class="edit-icon" @click="handleEditClick(item)" />
</div>
<div class="api-item-label">{{ t('ms.personal.createTime') }}</div>
<div class="api-item-value">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</div>
<div class="api-item-label">{{ t('ms.personal.expireTime') }}</div>
<div class="api-item-label">{{ t('ms.personal.validTime') }}</div>
<div class="api-item-value">
{{ item.forever ? t('ms.personal.forever') : dayjs(item.expireTime).format('YYYY-MM-DD HH:mm:ss') }}
<a-tooltip v-if="item.isExpire" :content="t('ms.personal.expiredTip')">
@ -63,16 +73,26 @@
</div>
</div>
<div class="flex items-center justify-between px-[16px]">
<MsTableMoreAction :list="actions" trigger="click" @select="handleMoreActionSelect($event, item)">
<div class="flex items-center gap-[8px]">
<a-button
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE', 'SYSTEM_PERSONAL_API_KEY:READ+DELETE']"
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE']"
size="mini"
type="outline"
class="arco-btn-outline--secondary"
@click="handleSetValidTime(item)"
>
{{ t('common.setting') }}
{{ t('ms.personal.validTime') }}
</a-button>
</MsTableMoreAction>
<a-button
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+DELETE']"
size="mini"
type="outline"
class="arco-btn-outline--danger"
@click="deleteApiKey(item)"
>
{{ t('common.delete') }}
</a-button>
</div>
<a-switch
v-model:model-value="item.enable"
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE']"
@ -142,8 +162,6 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import {
@ -168,6 +186,7 @@
interface APIKEYItem extends APIKEY {
isExpire: boolean;
desensitization: boolean;
showDescInput: boolean;
}
const apiKeyList = ref<APIKEYItem[]>([]);
const hasCratePermission = hasAnyPermission(['SYSTEM_PERSONAL_API_KEY:READ+ADD']);
@ -180,6 +199,7 @@
...item,
isExpire: item.forever ? false : item.expireTime < Date.now(),
desensitization: true,
showDescInput: false,
}));
} catch (error) {
// eslint-disable-next-line no-console
@ -208,23 +228,6 @@
}
}
const actions: ActionsItem[] = [
{
label: t('ms.personal.validTime'),
eventTag: 'time',
permission: ['SYSTEM_PERSONAL_API_KEY:READ+UPDATE'],
},
{
isDivider: true,
},
{
label: t('common.delete'),
danger: true,
eventTag: 'delete',
permission: ['SYSTEM_PERSONAL_API_KEY:READ+DELETE'],
},
];
function handleCopy(val: string) {
if (isSupported) {
copy(val);
@ -299,6 +302,30 @@
return false;
}
function handleEditClick(item: APIKEYItem) {
item.showDescInput = true;
nextTick(() => {
document.querySelector<HTMLInputElement>('.arco-textarea')?.focus();
});
}
async function handleDescChange(item: APIKEYItem) {
try {
loading.value = true;
await updateAPIKEY({
id: item.id || '',
description: item.description,
});
item.showDescInput = false;
Message.success(t('common.updateSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
const timeModalVisible = ref(false);
const defaultTimeForm = {
activeTimeType: 'forever',
@ -307,13 +334,14 @@
};
const timeForm = ref({ ...defaultTimeForm });
const timeFormRef = ref<FormInstance>();
const activeKey = ref<APIKEYItem>();
function handleTimeConfirm(done: (closed: boolean) => void) {
timeFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
await updateAPIKEY({
id: apiKeyList.value[0].id,
id: activeKey.value?.id || '',
description: timeForm.value.desc,
expireTime: timeForm.value.activeTimeType === 'forever' ? 0 : dayjs(timeForm.value.time).valueOf(),
forever: timeForm.value.activeTimeType === 'forever',
@ -337,17 +365,14 @@
timeForm.value = { ...defaultTimeForm };
}
function handleMoreActionSelect(item: ActionsItem, apiKey: APIKEYItem) {
if (item.eventTag === 'time') {
function handleSetValidTime(apiKey: APIKEYItem) {
activeKey.value = apiKey;
timeForm.value = {
activeTimeType: apiKey.forever ? 'forever' : 'custom',
time: apiKey.expireTime ? dayjs(apiKey.expireTime).format('YYYY-MM-DD HH:mm:ss') : '',
desc: apiKey.description,
};
timeModalVisible.value = true;
} else if (item.eventTag === 'delete') {
deleteApiKey(apiKey);
}
}
</script>
@ -407,4 +432,17 @@
}
}
}
.desc-line {
gap: 4px;
&:hover {
.edit-icon {
@apply visible;
}
}
.edit-icon {
@apply invisible cursor-pointer;
color: rgb(var(--primary-5));
}
}
</style>

View File

@ -95,6 +95,7 @@
isValidXml.value = false;
return;
}
isValidXml.value = true;
parsedXml.value = xmlDoc;
// XML icon
flattenedXml.value = new XmlBeautify({ parser: DOMParser })

View File

@ -326,28 +326,12 @@ export default defineComponent({
});
const renderPager = () => {
if (props.simple) {
return (
<span class={`${prefixCls}-simple-jumper`}>
{getPageItemElement('previous', { simple: true })}
<PageJumper
disabled={props.disabled}
current={computedCurrent.value}
size={mergedSize.value}
pages={pages.value}
simple
onChange={handleClick}
/>
{getPageItemElement('next', { simple: true })}
</span>
);
}
return (
<ul class={`${prefixCls}-list`}>
{getPageItemElement('previous', { simple: true })}
{pageList.value}
{props.showMore &&
!props.simple &&
getPageItemElement('more', {
key: 'more',
step: props.bufferSize * 2 + 1,
@ -396,7 +380,7 @@ export default defineComponent({
return (
<div class={cls.value}>
{props.showTotal && (
{props.showTotal && !props.simple && (
<span class={`${prefixCls}-total`}>
{slots.total?.({ total: props.total }) ?? t('msPagination.total', { total: props.total })}
</span>
@ -412,7 +396,7 @@ export default defineComponent({
/>
)}
{renderPager()}
{!props.simple && props.showJumper && (
{!props.simple && !props.simple && props.showJumper && (
<PageJumper
v-slots={{
'jumper-prepend': slots['jumper-prepend'],

View File

@ -232,7 +232,7 @@
v-if="!!attrs.showPagination"
size="small"
v-bind="(attrs.msPagination as MsPaginationI)"
hide-on-single-page
:simple="showBatchAction"
@change="pageChange"
@page-size-change="pageSizeChange"
/>
@ -469,7 +469,7 @@
});
const showBatchAction = computed(() => {
return selectedCount.value > 0 && attrs.selectable;
return selectedCount.value > 0 && !!attrs.selectable;
});
const handleBatchAction = (value: BatchActionParams) => {
@ -622,6 +622,13 @@
batchLeft.value = getBatchLeft();
});
watch(
() => props.columns,
() => {
initColumn();
}
);
defineExpose({
initColumn,
});

View File

@ -1,5 +1,5 @@
<template>
<div class="w-full">
<a-tooltip :content="allTagText" :disabled="innerModelValue.length === 0" :mouse-enter-delay="300">
<div :class="`flex w-full items-center ${props.class}`">
<a-input-tag
v-model:model-value="innerModelValue"
@ -34,7 +34,7 @@
<div v-if="isError" class="ml-[1px] flex justify-start text-[12px] text-[rgb(var(--danger-6))]">
{{ t('common.tagInputMaxLength', { number: props.maxLength }) }}
</div>
</div>
</a-tooltip>
</template>
<script setup lang="ts">
@ -85,6 +85,10 @@
innerInputValue.value.length > props.maxLength ||
(innerModelValue.value || []).some((item) => item.toString().length > props.maxLength)
);
const allTagText = computed(() => {
return (innerModelValue.value || []).join('、');
});
watch(
() => props.modelValue,
(val) => {

View File

@ -0,0 +1,37 @@
import { onBeforeRouteLeave } from 'vue-router';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import { hasAnyPermission } from '@/utils/permission';
import { useI18n } from './useI18n';
import useModal from './useModal';
export default function useLeaveTabUnSaveCheck(tabs: TabItem[], permissions?: string[]) {
const { openModal } = useModal();
const { t } = useI18n();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let isLeaving = false;
onBeforeRouteLeave((to, from, next) => {
if (!isLeaving && tabs.some((tab) => tab.unSaved) && hasAnyPermission(permissions || [])) {
isLeaving = true;
// 如果有未保存的tab则提示用户
openModal({
type: 'warning',
title: t('common.tip'),
content: t('apiTestDebug.unsavedLeave'),
hideCancel: false,
cancelText: t('common.stay'),
okText: t('common.leave'),
onBeforeOk: async () => {
next();
},
onCancel: () => {
isLeaving = false;
},
});
} else {
next();
}
});
}

View File

@ -4,7 +4,6 @@ import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
const isSave = ref(false);
const isRouteIntercepted = ref<boolean>(false);
// 离开页面确认提示
export default function useLeaveUnSaveTip() {

View File

@ -1,5 +1,4 @@
<template>
<div class="end-item">
<DefaultLayout
:logo="pageConfig.logoPlatform[0]?.url || defaultPlatformLogo"
:name="pageConfig.platformName"
@ -25,7 +24,6 @@
</div>
</template>
</DefaultLayout>
</div>
</template>
<script lang="ts" setup>
@ -45,15 +43,9 @@
</script>
<style lang="less" scoped>
.end-item {
:deep(.arco-menu-vertical) {
.arco-menu-inner {
justify-content: end;
}
}
}
.page {
height: 100vh;
@apply h-full;
background-color: var(--color-text-fff);
.content-wrapper {
display: flex;

View File

@ -47,7 +47,8 @@
<style lang="less" scoped>
.single-logo-layout {
height: 100vh;
@apply h-full;
background-color: var(--color-text-n9);
.body {
margin-top: 56px;

View File

@ -147,4 +147,8 @@ export default {
'common.value.notNull': 'The attribute value cannot be empty',
'common.nameNotNull': 'Name cannot be empty',
'common.namePlaceholder': 'Please enter the name and press Enter to save',
'common.unsavedLeave':
'The content of some tabs has not been saved. The unsaved content will be lost after leaving. Are you sure you want to leave?',
'common.image': 'Image',
'common.text': 'Text',
};

View File

@ -148,4 +148,7 @@ export default {
'common.value.notNull': '属性值不能为空',
'common.nameNotNull': '名称不能为空',
'common.namePlaceholder': '请输入名称,按回车键保存',
'common.unsavedLeave': '有标签页的内容未保存,离开后后未保存的内容将丢失,确定要离开吗?',
'common.image': '图片',
'common.text': '文本',
};

View File

@ -400,6 +400,7 @@ export interface ResponseResult {
transferStartTime: number;
vars: string;
assertions: any;
imageUrl?: string; // 返回为图片时的图片地址
}
export interface RequestResult {

View File

@ -36,8 +36,8 @@ export interface LocalConfig {
// 更新 APIKEY
export interface UpdateAPIKEYParams {
id: string;
forever: boolean;
expireTime: number;
forever?: boolean;
expireTime?: number;
description: string;
}
// APIKEY

View File

@ -12,7 +12,7 @@ import { getPackageType, getSystemVersion, getUserHasProjectPermission } from '@
import { getMenuList } from '@/api/modules/user';
import defaultSettings from '@/config/settings.json';
import { useI18n } from '@/hooks/useI18n';
import { NO_PROJECT_ROUTE_NAME, NO_RESOURCE_ROUTE_NAME, WHITE_LIST } from '@/router/constants';
import { NO_PROJECT_ROUTE_NAME } from '@/router/constants';
import { watchStyle, watchTheme } from '@/utils/theme';
import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config';

View File

@ -87,7 +87,7 @@
v-if="columnConfig.inputType === 'autoComplete'"
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabledExceptParam"
:data="columnConfig.autoCompleteParams?.filter((e) => e.isShow === true)"
:data="columnConfig.autoCompleteParams?.filter((e) => e.isShow !== false)"
class="ms-form-table-input"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
:filter-option="false"

View File

@ -42,6 +42,7 @@
layout: 'horizontal' | 'vertical';
response?: string; //
isDefinition?: boolean; //
isScenario?: boolean; //
disabled?: boolean;
}>();
const emit = defineEmits<{
@ -62,6 +63,10 @@
if (props.isDefinition) {
return [RequestConditionProcessor.SCRIPT, RequestConditionProcessor.SQL, RequestConditionProcessor.EXTRACT];
}
//
if (props.isScenario) {
return [RequestConditionProcessor.SCRIPT, RequestConditionProcessor.SQL];
}
return [RequestConditionProcessor.SCRIPT];
});
</script>

View File

@ -37,6 +37,7 @@
const props = defineProps<{
config: ExecuteConditionConfig;
isDefinition?: boolean; //
isScenario?: boolean; //
disabled?: boolean;
}>();
const emit = defineEmits<{
@ -48,9 +49,15 @@
const innerConfig = useVModel(props, 'config', emit);
const conditionTypes = computed(() => {
//
if (props.isDefinition) {
return [RequestConditionProcessor.SCRIPT, RequestConditionProcessor.SQL, RequestConditionProcessor.TIME_WAITING];
}
//
if (props.isScenario) {
return [RequestConditionProcessor.SCRIPT, RequestConditionProcessor.SQL];
}
//
return [RequestConditionProcessor.SCRIPT, RequestConditionProcessor.TIME_WAITING];
});
</script>

View File

@ -1,10 +1,23 @@
<template>
<div v-if="showImg">
<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 value="text">{{ t('common.text') }}</a-radio>
</a-radio-group>
</div>
<a-image v-show="showType === 'image'" :src="imageUrl"></a-image>
</div>
<MsCodeEditor
v-show="!showImg || showType === 'text'"
ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body"
:language="responseLanguage"
theme="vs"
height="100%"
:height="showImg ? 'calc(100% - 26px)' : '100%'"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
@ -27,6 +40,9 @@
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { downloadUrlFile } from '@/utils';
import { RequestResult } from '@/models/apiTest/common';
const props = defineProps<{
@ -40,6 +56,8 @@
(e: 'copy'): void;
}>();
const { t } = useI18n();
//
const responseLanguage = computed(() => {
if (props.requestResult) {
@ -56,6 +74,27 @@
}
return LanguageEnum.PLAINTEXT;
});
const showImg = computed(() => {
if (props.requestResult) {
return props.requestResult.responseResult.contentType.includes('image');
}
return false;
});
const imageUrl = computed(() => {
if (props.requestResult) {
return `data:${props.requestResult?.responseResult.contentType};base64,${props.requestResult?.responseResult.imageUrl}`;
}
return '';
});
const showType = ref<'image' | 'text'>('image');
function handleDownload() {
if (imageUrl.value) {
downloadUrlFile(imageUrl.value, `response.${props.requestResult?.responseResult.contentType.split('/')[1]}`);
}
}
</script>
<style scoped></style>

View File

@ -122,6 +122,7 @@
uploadTempFile,
} from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n';
import useLeaveTabUnSaveCheck from '@/hooks/useLeaveTabUnSaveCheck';
import useModal from '@/hooks/useModal';
import { parseCurlScript } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
@ -346,34 +347,7 @@
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let isLeaving = false;
onBeforeRouteLeave((to, from, next) => {
if (
!isLeaving &&
debugTabs.value.some((tab) => tab.unSaved) &&
hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEBUG:READ+UPDATE'])
) {
isLeaving = true;
//
openModal({
type: 'warning',
title: t('common.tip'),
content: t('apiTestDebug.unsavedLeave'),
hideCancel: false,
cancelText: t('common.stay'),
okText: t('common.leave'),
onBeforeOk: async () => {
next();
},
onCancel: () => {
isLeaving = false;
},
});
} else {
next();
}
});
useLeaveTabUnSaveCheck(debugTabs.value, ['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEBUG:READ+UPDATE']);
</script>
<style lang="less" scoped></style>

View File

@ -436,6 +436,9 @@
}
:deep(.ms-api-tab-nav) {
@apply h-full;
.arco-tabs {
@apply border-b-0;
}
.arco-tabs-nav {
border-bottom: 1px solid var(--color-text-n8);
}

View File

@ -659,7 +659,7 @@
case RequestBodyFormat.FORM_DATA:
return (previewDetail.value.body.formDataBody?.formValues || []).map((e) => ({
...e,
value: e.paramType === RequestParamsType.FILE ? e.files?.map((file) => file.fileName).join('\n') : e.value,
value: e.paramType === RequestParamsType.FILE ? e.files?.map((file) => file.fileName).join('') : e.value,
}));
case RequestBodyFormat.WWW_FORM:
return previewDetail.value.body.wwwFormBody?.formValues || [];

View File

@ -66,7 +66,7 @@
</a-select>
</a-form-item>
<a-form-item field="tags" :label="t('common.tag')">
<MsTagsInput v-model:model-value="detailForm.tags" />
<MsTagsInput v-model:model-value="detailForm.tags" :max-tag-count="1" />
</a-form-item>
</div>
</a-form>

View File

@ -56,6 +56,7 @@
</template>
<script setup lang="ts">
import { onBeforeRouteLeave } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
@ -68,6 +69,8 @@
// import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n';
import useLeaveTabUnSaveCheck from '@/hooks/useLeaveTabUnSaveCheck';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
@ -95,6 +98,7 @@
}>();
const appStore = useAppStore();
const { t } = useI18n();
const { openModal } = useModal();
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
@ -299,6 +303,13 @@
initProtocolList();
});
useLeaveTabUnSaveCheck(apiTabs.value, [
'PROJECT_API_DEFINITION:READ+ADD',
'PROJECT_API_DEFINITION:READ+UPDATE',
'PROJECT_API_DEFINITION_CASE:READ+ADD',
'PROJECT_API_DEFINITION_CASE:READ+UPDATE',
]);
/** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams));

View File

@ -11,6 +11,7 @@
:max-length="255"
:placeholder="t('apiScenario.namePlaceholder')"
allow-clear
@input="() => emit('change')"
/>
</a-form-item>
<a-form-item :label="t('apiScenario.belongModule')" class="mb-[16px]">
@ -25,10 +26,15 @@
},
}"
allow-search
@change="() => emit('change')"
/>
</a-form-item>
<a-form-item :label="t('apiScenario.scenarioLevel')">
<a-select v-model:model-value="scenario.priority" :placeholder="t('common.pleaseSelect')">
<a-select
v-model:model-value="scenario.priority"
:placeholder="t('common.pleaseSelect')"
@change="() => emit('change')"
>
<template #label>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="scenario.priority" /></span>
</template>
@ -42,6 +48,7 @@
v-model:model-value="scenario.status"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full"
@change="() => emit('change')"
>
<template #label>
<apiStatus :status="scenario.status" />
@ -52,13 +59,14 @@
</a-select>
</a-form-item>
<a-form-item :label="t('common.tag')" class="mb-[16px]">
<MsTagsInput v-model:model-value="scenario.tags" />
<MsTagsInput v-model:model-value="scenario.tags" @change="() => emit('change')" />
</a-form-item>
<a-form-item :label="t('common.desc')" class="mb-[16px]">
<a-textarea
v-model:model-value="scenario.description"
:max-length="500"
:placeholder="t('apiScenario.descPlaceholder')"
@input="() => emit('change')"
/>
</a-form-item>
<template v-if="props.isEdit">
@ -96,6 +104,8 @@
moduleTree: ModuleTreeNode[]; //
isEdit?: boolean;
}>();
const emit = defineEmits(['change']);
const scenario = defineModel<ScenarioDetail | Scenario>('scenario', {
required: true,
});

View File

@ -33,7 +33,6 @@
v-model:model-value="requestVModel.customizeRequestEnvEnable"
class="w-[150px]"
:disabled="props.step?.isQuoteScenarioStep"
popup-container=".customApiDrawer-title-right"
@change="handleUseEnvChange"
>
<template #prefix>
@ -102,7 +101,7 @@
</a-input>
</a-input-group>
</div>
<div>
<div v-permission="[props.permissionMap?.execute]">
<a-dropdown-button
v-if="hasLocalExec"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
@ -395,8 +394,6 @@
detailLoading?: boolean; //
permissionMap?: {
execute: string;
create: string;
update: string;
};
stepResponses?: Record<string | number, RequestResult[]>;
fileParams?: ScenarioStepFileParams;
@ -1115,6 +1112,7 @@
url: props.request.path, // path
activeTab: contentTabList.value[0].value,
responseActiveTab: ResponseComposition.BODY,
stepId: props.step?.uniqueId || '',
isNew: false,
});
if (_stepType.value.isQuoteApi) {

View File

@ -60,6 +60,7 @@
:disabled="!isEditableApi"
allow-clear
/>
<div v-permission="[props.permissionMap?.execute]">
<a-dropdown-button
v-if="hasLocalExec"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
@ -82,6 +83,7 @@
</a-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
</div>
</div>
<div class="px-[16px]">
<MsTab
v-model:active-key="requestVModel.activeTab"
@ -306,6 +308,9 @@
request?: RequestParam; //
stepResponses?: Record<string | number, RequestResult[]>;
fileParams?: ScenarioStepFileParams;
permissionMap?: {
execute: string;
};
}>();
const emit = defineEmits<{
(e: 'applyStep', request: RequestParam): void;

View File

@ -94,12 +94,9 @@
const moduleCountMap = ref<Record<string, number>>({});
const selectedKeys = ref<string[]>([]);
const folderText = computed(() => {
if (props.type === 'api') {
if (props.type === 'api' || props.type === 'case') {
return t('apiTestManagement.allApi');
}
if (props.type === 'case') {
return t('apiTestManagement.allCase');
}
if (props.type === 'scenario') {
return t('apiScenario.tree.folder.allScenario');
}

View File

@ -124,7 +124,16 @@
const keyword = ref('');
const columns: MsTableColumn = [
const tableConfig = {
scroll: { x: 700 },
selectable: true,
showSelectorAll: false,
heightUsed: 300,
};
//
const useApiTable = useTable(getDefinitionPage, {
...tableConfig,
columns: [
{
title: 'ID',
dataIndex: 'num',
@ -179,18 +188,72 @@
isStringTag: true,
width: 150,
},
];
const tableConfig = {
columns,
scroll: { x: 700 },
selectable: true,
showSelectorAll: false,
heightUsed: 300,
};
//
const useApiTable = useTable(getDefinitionPage, tableConfig);
],
});
//
const useCaseTable = useTable(getCasePage, tableConfig);
const useCaseTable = useTable(getCasePage, {
...tableConfig,
columns: [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 130,
ellipsis: true,
showTooltip: true,
},
{
title: 'case.caseName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
{
title: 'case.caseLevel',
dataIndex: 'priority',
slotName: 'caseLevel',
titleSlotName: 'caseLevelFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
{
title: 'apiTestManagement.apiStatus',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
{
title: 'apiTestManagement.path',
dataIndex: 'path',
showTooltip: true,
width: 150,
},
{
title: 'common.tag',
dataIndex: 'tags',
isTag: true,
isStringTag: true,
},
],
});
//
const useScenarioTable = useTable(getScenarioPage, {
...tableConfig,

View File

@ -36,7 +36,9 @@
<style lang="less" scoped>
.condition {
overflow: overlay;
.ms-scroll-bar();
overflow: auto;
width: 100%;
height: 700px;
flex-shrink: 0;

View File

@ -252,7 +252,11 @@
<template v-if="hasAnyPermission(['PROJECT_API_SCENARIO:READ+ADD'])" #empty>
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('api_scenario.table.tableNoDataAndPlease') }}
<MsButton class="float-right ml-[8px]" @click="emit('createScenario')">
<MsButton
v-permission="['PROJECT_API_SCENARIO:READ+ADD']"
class="float-right ml-[8px]"
@click="emit('createScenario')"
>
{{ t('apiScenario.createScenario') }}
</MsButton>
</div>

View File

@ -1,4 +1,5 @@
<template>
<div>
<a-dropdown
v-model:popup-visible="visible"
:position="props.position || 'bottom'"
@ -9,7 +10,10 @@
<slot></slot>
<template #content>
<a-dgroup :title="t('apiScenario.requestScenario')">
<a-doption :value="ScenarioAddStepActionType.IMPORT_SYSTEM_API">
<a-doption
v-permission="['PROJECT_API_SCENARIO:READ+IMPORT']"
:value="ScenarioAddStepActionType.IMPORT_SYSTEM_API"
>
{{ t('apiScenario.importSystemApi') }}
</a-doption>
<a-doption :value="ScenarioAddStepActionType.CUSTOM_API">
@ -38,6 +42,7 @@
</a-dgroup>
</template>
</a-dropdown>
</div>
</template>
<script setup lang="ts">

View File

@ -1,4 +1,5 @@
<template>
<div>
<a-trigger
trigger="click"
class="arco-trigger-menu absolute"
@ -59,6 +60,7 @@
</div>
</template>
</a-trigger>
</div>
</template>
<script setup lang="ts">

View File

@ -180,6 +180,7 @@
"
v-model:selected-keys="selectedKeys"
v-model:steps="steps"
v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']"
:step="step"
@click="setFocusNodeKey(step.uniqueId)"
@other-create="handleOtherCreate"
@ -223,6 +224,7 @@
<createStepActions
v-model:selected-keys="selectedKeys"
v-model:steps="steps"
v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']"
@add-done="handleAddStepDone"
@other-create="handleOtherCreate"
>
@ -239,6 +241,7 @@
:file-params="currentStepFileParams"
:step="activeStep"
:step-responses="scenario.stepResponses"
:permission-map="permissionMap"
@add-step="addCustomApiStep"
@apply-step="applyApiStep"
@stop-debug="handleStopExecute(activeStep)"
@ -250,6 +253,7 @@
:request="currentStepDetail as unknown as RequestParam"
:file-params="currentStepFileParams"
:step-responses="scenario.stepResponses"
:permission-map="permissionMap"
@apply-step="applyApiStep"
@delete-step="deleteCaseStep(activeStep)"
@stop-debug="handleStopExecute(activeStep)"
@ -489,6 +493,9 @@
const localExecuteUrl = inject<Ref<string>>('localExecuteUrl');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const permissionMap = {
execute: 'PROJECT_API_SCENARIO:READ+EXECUTE',
};
const loading = ref(false);
const treeRef = ref<InstanceType<typeof MsTree>>();
const focusStepKey = ref<string | number>(''); // key
@ -1375,6 +1382,10 @@
deleteFileIds: request.deleteFileIds,
unLinkFileIds: request.unLinkFileIds,
};
activeStep.value.config = {
...activeStep.value.config,
method: request.method,
};
emit('updateResource', request.uploadFileIds, request.linkFileIds);
activeStep.value = undefined;
}

View File

@ -61,7 +61,12 @@
<div class="p-[16px]">
<!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<baseInfo ref="baseInfoRef" :scenario="scenario as Scenario" :module-tree="props.moduleTree" />
<baseInfo
ref="baseInfoRef"
:scenario="scenario as Scenario"
:module-tree="props.moduleTree"
@change="scenario.unSaved = true"
/>
<!-- TODO:第一版先不做依赖 -->
<!-- <div class="mb-[8px] flex items-center">
<div class="text-[var(--color-text-2)]">

View File

@ -20,11 +20,19 @@
<environmentSelect v-model:current-env-config="currentEnvConfig" />
<executeButton
ref="executeButtonRef"
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
:execute-loading="activeScenarioTab.executeLoading"
@execute="handleExecute"
@stop-debug="handleStopExecute"
/>
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
<a-button
v-permission="
activeScenarioTab.isNew ? ['PROJECT_API_SCENARIO:READ+ADD'] : ['PROJECT_API_SCENARIO:READ+UPDATE']
"
type="primary"
:loading="saveLoading"
@click="saveScenario"
>
{{ t('common.save') }}
</a-button>
</div>
@ -118,6 +126,7 @@
} from '@/api/modules/api-test/scenario';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n';
import useLeaveTabUnSaveCheck from '@/hooks/useLeaveTabUnSaveCheck';
import router from '@/router';
import useAppStore from '@/store/modules/app';
import { filterTree, getGenerateId, mapTree } from '@/utils';
@ -458,6 +467,25 @@
scenarioDetail.id = res.id;
if (!scenarioDetail.steps) {
scenarioDetail.steps = [];
} else {
scenarioDetail.steps = mapTree(scenarioDetail.steps, (node) => {
if (
node.parent &&
node.parent.stepType === ScenarioStepType.API_SCENARIO &&
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.parent.refType)
) {
//
node.isQuoteScenarioStep = true; //
node.isRefScenarioStep = node.parent.refType === ScenarioStepRefType.REF; //
node.draggable = false; //
} else if (node.parent) {
//
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; //
}
node.uniqueId = getGenerateId();
return node;
});
}
const index = scenarioTabs.value.findIndex((e) => e.id === activeScenarioTab.value.id);
if (index !== -1) {
@ -526,6 +554,8 @@
}
});
useLeaveTabUnSaveCheck(scenarioTabs.value, ['PROJECT_API_SCENARIO:READ+ADD', 'PROJECT_API_SCENARIO:READ+UPDATE']);
const hasLocalExec = computed(() => executeButtonRef.value?.hasLocalExec);
const localExecuteUrl = computed(() => executeButtonRef.value?.localExecuteUrl);
const isPriorityLocalExec = computed(() => executeButtonRef.value?.isPriorityLocalExec);

View File

@ -87,7 +87,7 @@
import { GetLoginLogoUrl } from '@/api/requrls/setting/config';
import { useI18n } from '@/hooks/useI18n';
import useLoading from '@/hooks/useLoading';
import { NO_PROJECT_ROUTE_NAME } from '@/router/constants';
import { NO_PROJECT_ROUTE_NAME, NO_RESOURCE_ROUTE_NAME } from '@/router/constants';
import { useAppStore, useUserStore } from '@/store';
import { encrypted } from '@/utils';
import { setLoginExpires } from '@/utils/auth';
@ -160,8 +160,18 @@
const { username, password } = values;
loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : '';
if (!appStore.currentProjectId || appStore.currentProjectId === 'no_such_project') {
// &/
router.push({
name: NO_PROJECT_ROUTE_NAME,
});
return;
}
const { redirect, ...othersQuery } = router.currentRoute.value.query;
const redirectHasPermission = redirect && routerNameHasPermission(redirect as string, router.getRoutes());
const redirectHasPermission =
redirect &&
![NO_RESOURCE_ROUTE_NAME, NO_PROJECT_ROUTE_NAME].includes(redirect as string) &&
routerNameHasPermission(redirect as string, router.getRoutes());
const currentRouteName = getFirstRouteNameByPermission(router.getRoutes());
const res = await getProjectInfo(appStore.currentProjectId);
if (!res || res.deleted) {

View File

@ -5,9 +5,9 @@
</div>
<div
v-if="props.robot.platform === 'IN_SITE'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] p-[12px] text-[14px]"
class="preview-rounded h-full w-[400px] bg-[var(--color-text-n9)] p-[12px] text-[14px]"
>
<div class="preview-rounded bg-white">
<div class="preview-rounded h-full overflow-scroll bg-white">
<div
v-if="!props.isUpdatePreview"
class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[16px]"
@ -19,9 +19,9 @@
</div>
<div class="flex gap-[12px] p-[16px]">
<a-avatar>MS</a-avatar>
<div class="flex flex-1 flex-col">
<div class="flex flex-1 flex-col overflow-hidden">
<div class="font-medium text-[var(--color-text-1)]" v-html="subject || '-'"></div>
<div class="mt-[4px] text-[var(--color-text-2)]" v-html="template || '-'"></div>
<div class="mt-[4px] flex-1 text-[var(--color-text-2)]" v-html="template || '-'"></div>
<div class="text-[var(--color-text-4)]">{{ dayjs().format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
</div>
@ -29,7 +29,7 @@
</div>
<div
v-else-if="props.robot.platform === 'MAIL'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] p-[12px] text-[14px]"
class="preview-rounded h-full w-[400px] overflow-scroll bg-[var(--color-text-n9)] p-[12px] text-[14px]"
>
<div class="mb-[4px] text-[16px] font-medium leading-[24px] text-[var(--color-text-1)]" v-html="subject || '-'">
</div>
@ -48,7 +48,7 @@
</div>
<div
v-else-if="props.robot.platform === 'WE_COM'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] p-[12px] text-[14px]"
class="preview-rounded h-full w-[400px] overflow-scroll bg-[var(--color-text-n9)] p-[12px] text-[14px]"
>
<div class="preview-rounded bg-white">
<div class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[16px_16px_8px_16px]">
@ -71,7 +71,7 @@
</div>
<div
v-else-if="props.robot.platform === 'DING_TALK'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] text-[14px]"
class="preview-rounded h-full w-[400px] overflow-scroll bg-[var(--color-text-n9)] text-[14px]"
>
<div class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[12px_12px_4px_12px]">
<div class="flex items-center gap-[4px] text-[14px] font-medium text-[var(--color-text-1)]">
@ -114,7 +114,7 @@
</div>
<div
v-else-if="props.robot.platform === 'LARK'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] text-[14px]"
class="preview-rounded h-full w-[400px] overflow-scroll bg-[var(--color-text-n9)] text-[14px]"
>
<div class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[12px_12px_4px_12px]">
<div class="flex items-center gap-[4px] text-[14px] font-medium text-[var(--color-text-1)]">
@ -149,7 +149,7 @@
</div>
<div
v-else-if="props.robot.platform === 'CUSTOM'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] p-[12px] text-[14px]"
class="preview-rounded h-full w-[400px] overflow-scroll bg-[var(--color-text-n9)] p-[12px] text-[14px]"
>
<div class="preview-rounded bg-white">
<div class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[16px_16px_8px_16px]">
@ -232,7 +232,7 @@
) {
previewName = `MeterSphere ${previewName}`;
}
return previewName
return previewName;
});
const template = computed(() => {
@ -249,4 +249,8 @@
border-radius: var(--border-radius-small);
}
.overflow-scroll {
@apply overflow-y-auto;
.ms-scroll-bar();
}
</style>

View File

@ -133,6 +133,7 @@
:max-length="255"
:placeholder="t('project.messageManagement.namePlaceholder')"
allow-clear
class="w-[732px]"
></a-input>
</a-form-item>
<a-form-item
@ -149,7 +150,11 @@
</a-radio>
</a-radio-group>
<template v-if="robotForm.type === 'CUSTOM'">
<MsFormItemSub :text="t('project.messageManagement.dingTalkCustomTip')" :show-fill-icon="false">
<MsFormItemSub
:text="t('project.messageManagement.dingTalkCustomTip')"
:show-fill-icon="false"
class="mb-[16px]"
>
<MsButton
type="text"
class="ml-[8px] !text-[12px] leading-[16px]"
@ -159,7 +164,7 @@
{{ t('project.messageManagement.noticeDetail') }}
</MsButton>
</MsFormItemSub>
<a-alert :title="t('project.messageManagement.dingTalkCustomTitle')">
<a-alert :title="t('project.messageManagement.dingTalkCustomTitle')" class="w-[732px]">
<div class="text-[var(--color-text-2)]">{{ t('project.messageManagement.dingTalkCustomContent1') }}</div>
<div class="text-[var(--color-text-2)]">
{{ t('project.messageManagement.dingTalkCustomContent2', { at: '@' }) }}
@ -168,7 +173,11 @@
</a-alert>
</template>
<template v-else>
<MsFormItemSub :text="t('project.messageManagement.dingTalkEnterpriseTip')" :show-fill-icon="false">
<MsFormItemSub
:text="t('project.messageManagement.dingTalkEnterpriseTip')"
:show-fill-icon="false"
class="mb-[16px]"
>
<MsButton
type="text"
class="ml-[8px] !text-[12px] leading-[16px]"
@ -182,7 +191,7 @@
{{ t('project.messageManagement.helpDoc') }}
</MsButton>
</MsFormItemSub>
<a-alert :title="t('project.messageManagement.dingTalkEnterpriseTitle')">
<a-alert :title="t('project.messageManagement.dingTalkEnterpriseTitle')" class="w-[732px]">
<div class="text-[var(--color-text-2)]">
{{ t('project.messageManagement.dingTalkEnterpriseContent1', { at: '@' }) }}
</div>
@ -205,6 +214,7 @@
:max-length="255"
:placeholder="t('project.messageManagement.appKeyPlaceholder')"
allow-clear
class="w-[732px]"
></a-input>
</a-form-item>
<a-form-item
@ -219,6 +229,7 @@
:max-length="255"
:placeholder="t('project.messageManagement.appSecretPlaceholder')"
allow-clear
class="w-[732px]"
></a-input>
</a-form-item>
</template>
@ -248,6 +259,7 @@
}
)
"
class="w-[732px]"
allow-clear
></a-input>
</a-form-item>
@ -358,7 +370,7 @@
projectId: appStore.currentProjectId,
name: '',
platform: 'WE_COM',
enable: false,
enable: true,
webhook: '',
type: 'CUSTOM',
appKey: '',

View File

@ -24,7 +24,7 @@
</a-tooltip>
</div>
<a-select v-model:model-value="fieldType" class="my-[8px]" :options="fieldTypeOptions"></a-select>
<a-spin class="relative h-[calc(100%-69px)] w-full" :loading="fieldLoading">
<a-spin class="relative h-[calc(100%-70px)] w-full" :loading="fieldLoading">
<div :class="`field-out-container ${containerStatusClass}`">
<div ref="fieldListRef" class="field-container">
<div v-for="field of filterFields" :key="field.id" class="field-item" @click="addField(field)">
@ -45,7 +45,7 @@
</div>
</template>
</a-popover>
<MsButton type="icon" class="field-plus">
<MsButton type="icon" class="field-plus" @click="addField(field)">
<MsIcon type="icon-icon_add_outlined" size="14"></MsIcon>
</MsButton>
</div>
@ -68,7 +68,7 @@
}}
</div>
</div>
<div class="flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]">
<div class="flex-1 overflow-hidden rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]">
<div class="mb-[8px] text-[var(--color-text-4)]">{{ t('project.messageManagement.title') }}</div>
<a-textarea
ref="subjectInputRef"
@ -77,20 +77,22 @@
:auto-size="{ minRows: 3, maxRows: 3 }"
:max-length="1000"
:disabled="saveLoading"
class="break-keep"
@focus="focusTarget = 'subject'"
/>
<div class="mb-[8px] mt-[16px] text-[var(--color-text-4)]">{{ t('project.messageManagement.content') }}</div>
<div v-if="template.length > 0 || focusTarget === 'template'" class="h-[calc(100%-156px)]">
<a-textarea
v-if="template.length > 0 || focusTarget === 'template'"
ref="templateInputRef"
v-model:model-value="template"
class="h-[calc(100%-156px)]"
class="h-full overflow-scroll break-keep"
:max-length="1000"
auto-size
:disabled="saveLoading"
@focus="focusTarget = 'template'"
@blur="focusTarget = null"
/>
</div>
<div
v-else
class="flex h-[calc(100%-156px)] flex-col items-center gap-[16px] bg-white"
@ -105,20 +107,21 @@
<div class="mb-[8px] font-medium text-[var(--color-text-1)]">
{{ t('project.messageManagement.updatePreview') }}
</div>
<div v-if="messageDetail" class="h-[calc(100%-30px)] overflow-hidden">
<MessagePreview
v-if="messageDetail"
:robot="{
...messageDetail,
template,
subject,
}"
:fields="fields"
:function-name="'接口测试'"
:event-name="'创建任务'"
function-name=""
event-name=""
is-update-preview
/>
</div>
</div>
</div>
</MsCard>
</template>
@ -342,6 +345,10 @@
}
}
}
.overflow-scroll {
@apply overflow-y-auto;
.ms-scroll-bar();
}
.content-empty-img {
margin-top: 100px;
width: 160px;