fix(all): 修复部分 bug
This commit is contained in:
parent
c7bc5ed7f5
commit
41ccb313aa
|
@ -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);
|
||||
|
|
|
@ -387,6 +387,7 @@
|
|||
}
|
||||
}
|
||||
.arco-form-item-message {
|
||||
margin-bottom: 16px; // 设计要求表单输入行报错信息距离下一个表单输入行间距为16px
|
||||
width: 100%;
|
||||
}
|
||||
.hidden-item {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
isValidXml.value = false;
|
||||
return;
|
||||
}
|
||||
isValidXml.value = true;
|
||||
parsedXml.value = xmlDoc;
|
||||
// 先将 XML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
||||
flattenedXml.value = new XmlBeautify({ parser: DOMParser })
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -148,4 +148,7 @@ export default {
|
|||
'common.value.notNull': '属性值不能为空',
|
||||
'common.nameNotNull': '名称不能为空',
|
||||
'common.namePlaceholder': '请输入名称,按回车键保存',
|
||||
'common.unsavedLeave': '有标签页的内容未保存,离开后后未保存的内容将丢失,确定要离开吗?',
|
||||
'common.image': '图片',
|
||||
'common.text': '文本',
|
||||
};
|
||||
|
|
|
@ -400,6 +400,7 @@ export interface ResponseResult {
|
|||
transferStartTime: number;
|
||||
vars: string;
|
||||
assertions: any;
|
||||
imageUrl?: string; // 返回为图片时的图片地址
|
||||
}
|
||||
|
||||
export interface RequestResult {
|
||||
|
|
|
@ -36,8 +36,8 @@ export interface LocalConfig {
|
|||
// 更新 APIKEY
|
||||
export interface UpdateAPIKEYParams {
|
||||
id: string;
|
||||
forever: boolean;
|
||||
expireTime: number;
|
||||
forever?: boolean;
|
||||
expireTime?: number;
|
||||
description: string;
|
||||
}
|
||||
// APIKEY
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 || [];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -36,7 +36,9 @@
|
|||
|
||||
<style lang="less" scoped>
|
||||
.condition {
|
||||
overflow: overlay;
|
||||
.ms-scroll-bar();
|
||||
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)]">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue