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

View File

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

View File

@ -47,14 +47,24 @@
</div> </div>
<div class="px-[16px]"> <div class="px-[16px]">
<div class="api-item-label">{{ t('ms.personal.desc') }}</div> <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"> <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> </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-label">{{ t('ms.personal.createTime') }}</div>
<div class="api-item-value"> <div class="api-item-value">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }} {{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</div> </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"> <div class="api-item-value">
{{ item.forever ? t('ms.personal.forever') : dayjs(item.expireTime).format('YYYY-MM-DD HH:mm:ss') }} {{ 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')"> <a-tooltip v-if="item.isExpire" :content="t('ms.personal.expiredTip')">
@ -63,16 +73,26 @@
</div> </div>
</div> </div>
<div class="flex items-center justify-between px-[16px]"> <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 <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" size="mini"
type="outline" type="outline"
class="arco-btn-outline--secondary" class="arco-btn-outline--secondary"
@click="handleSetValidTime(item)"
> >
{{ t('common.setting') }} {{ t('ms.personal.validTime') }}
</a-button> </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 <a-switch
v-model:model-value="item.enable" v-model:model-value="item.enable"
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE']" v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE']"
@ -142,8 +162,6 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { import {
@ -168,6 +186,7 @@
interface APIKEYItem extends APIKEY { interface APIKEYItem extends APIKEY {
isExpire: boolean; isExpire: boolean;
desensitization: boolean; desensitization: boolean;
showDescInput: boolean;
} }
const apiKeyList = ref<APIKEYItem[]>([]); const apiKeyList = ref<APIKEYItem[]>([]);
const hasCratePermission = hasAnyPermission(['SYSTEM_PERSONAL_API_KEY:READ+ADD']); const hasCratePermission = hasAnyPermission(['SYSTEM_PERSONAL_API_KEY:READ+ADD']);
@ -180,6 +199,7 @@
...item, ...item,
isExpire: item.forever ? false : item.expireTime < Date.now(), isExpire: item.forever ? false : item.expireTime < Date.now(),
desensitization: true, desensitization: true,
showDescInput: false,
})); }));
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // 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) { function handleCopy(val: string) {
if (isSupported) { if (isSupported) {
copy(val); copy(val);
@ -299,6 +302,30 @@
return false; 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 timeModalVisible = ref(false);
const defaultTimeForm = { const defaultTimeForm = {
activeTimeType: 'forever', activeTimeType: 'forever',
@ -307,13 +334,14 @@
}; };
const timeForm = ref({ ...defaultTimeForm }); const timeForm = ref({ ...defaultTimeForm });
const timeFormRef = ref<FormInstance>(); const timeFormRef = ref<FormInstance>();
const activeKey = ref<APIKEYItem>();
function handleTimeConfirm(done: (closed: boolean) => void) { function handleTimeConfirm(done: (closed: boolean) => void) {
timeFormRef.value?.validate(async (errors) => { timeFormRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
try { try {
await updateAPIKEY({ await updateAPIKEY({
id: apiKeyList.value[0].id, id: activeKey.value?.id || '',
description: timeForm.value.desc, description: timeForm.value.desc,
expireTime: timeForm.value.activeTimeType === 'forever' ? 0 : dayjs(timeForm.value.time).valueOf(), expireTime: timeForm.value.activeTimeType === 'forever' ? 0 : dayjs(timeForm.value.time).valueOf(),
forever: timeForm.value.activeTimeType === 'forever', forever: timeForm.value.activeTimeType === 'forever',
@ -337,17 +365,14 @@
timeForm.value = { ...defaultTimeForm }; timeForm.value = { ...defaultTimeForm };
} }
function handleMoreActionSelect(item: ActionsItem, apiKey: APIKEYItem) { function handleSetValidTime(apiKey: APIKEYItem) {
if (item.eventTag === 'time') { activeKey.value = apiKey;
timeForm.value = { timeForm.value = {
activeTimeType: apiKey.forever ? 'forever' : 'custom', activeTimeType: apiKey.forever ? 'forever' : 'custom',
time: apiKey.expireTime ? dayjs(apiKey.expireTime).format('YYYY-MM-DD HH:mm:ss') : '', time: apiKey.expireTime ? dayjs(apiKey.expireTime).format('YYYY-MM-DD HH:mm:ss') : '',
desc: apiKey.description, desc: apiKey.description,
}; };
timeModalVisible.value = true; timeModalVisible.value = true;
} else if (item.eventTag === 'delete') {
deleteApiKey(apiKey);
}
} }
</script> </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> </style>

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<template> <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}`"> <div :class="`flex w-full items-center ${props.class}`">
<a-input-tag <a-input-tag
v-model:model-value="innerModelValue" 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))]"> <div v-if="isError" class="ml-[1px] flex justify-start text-[12px] text-[rgb(var(--danger-6))]">
{{ t('common.tagInputMaxLength', { number: props.maxLength }) }} {{ t('common.tagInputMaxLength', { number: props.maxLength }) }}
</div> </div>
</div> </a-tooltip>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -85,6 +85,10 @@
innerInputValue.value.length > props.maxLength || innerInputValue.value.length > props.maxLength ||
(innerModelValue.value || []).some((item) => item.toString().length > props.maxLength) (innerModelValue.value || []).some((item) => item.toString().length > props.maxLength)
); );
const allTagText = computed(() => {
return (innerModelValue.value || []).join('、');
});
watch( watch(
() => props.modelValue, () => props.modelValue,
(val) => { (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'; import useModal from '@/hooks/useModal';
const isSave = ref(false); const isSave = ref(false);
const isRouteIntercepted = ref<boolean>(false);
// 离开页面确认提示 // 离开页面确认提示
export default function useLeaveUnSaveTip() { export default function useLeaveUnSaveTip() {

View File

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

View File

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

View File

@ -147,4 +147,8 @@ export default {
'common.value.notNull': 'The attribute value cannot be empty', 'common.value.notNull': 'The attribute value cannot be empty',
'common.nameNotNull': 'Name cannot be empty', 'common.nameNotNull': 'Name cannot be empty',
'common.namePlaceholder': 'Please enter the name and press Enter to save', '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.value.notNull': '属性值不能为空',
'common.nameNotNull': '名称不能为空', 'common.nameNotNull': '名称不能为空',
'common.namePlaceholder': '请输入名称,按回车键保存', 'common.namePlaceholder': '请输入名称,按回车键保存',
'common.unsavedLeave': '有标签页的内容未保存,离开后后未保存的内容将丢失,确定要离开吗?',
'common.image': '图片',
'common.text': '文本',
}; };

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import { getPackageType, getSystemVersion, getUserHasProjectPermission } from '@
import { getMenuList } from '@/api/modules/user'; import { getMenuList } from '@/api/modules/user';
import defaultSettings from '@/config/settings.json'; import defaultSettings from '@/config/settings.json';
import { useI18n } from '@/hooks/useI18n'; 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 { watchStyle, watchTheme } from '@/utils/theme';
import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config'; import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config';

View File

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

View File

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

View File

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

View File

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

View File

@ -122,6 +122,7 @@
uploadTempFile, uploadTempFile,
} from '@/api/modules/api-test/debug'; } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useLeaveTabUnSaveCheck from '@/hooks/useLeaveTabUnSaveCheck';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import { parseCurlScript } from '@/utils'; import { parseCurlScript } from '@/utils';
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
@ -346,34 +347,7 @@
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars useLeaveTabUnSaveCheck(debugTabs.value, ['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEBUG:READ+UPDATE']);
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();
}
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

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

View File

@ -659,7 +659,7 @@
case RequestBodyFormat.FORM_DATA: case RequestBodyFormat.FORM_DATA:
return (previewDetail.value.body.formDataBody?.formValues || []).map((e) => ({ return (previewDetail.value.body.formDataBody?.formValues || []).map((e) => ({
...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: case RequestBodyFormat.WWW_FORM:
return previewDetail.value.body.wwwFormBody?.formValues || []; return previewDetail.value.body.wwwFormBody?.formValues || [];

View File

@ -66,7 +66,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item field="tags" :label="t('common.tag')"> <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> </a-form-item>
</div> </div>
</a-form> </a-form>

View File

@ -56,6 +56,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteLeave } from 'vue-router';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue'; 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 MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { getProtocolList } from '@/api/modules/api-test/common'; import { getProtocolList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useLeaveTabUnSaveCheck from '@/hooks/useLeaveTabUnSaveCheck';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
@ -95,6 +98,7 @@
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal();
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi'); const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
@ -299,6 +303,13 @@
initProtocolList(); 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('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams)); provide('defaultCaseParams', readonly(defaultCaseParams));

View File

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

View File

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

View File

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

View File

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

View File

@ -124,7 +124,16 @@
const keyword = ref(''); 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', title: 'ID',
dataIndex: 'num', dataIndex: 'num',
@ -179,18 +188,72 @@
isStringTag: true, isStringTag: true,
width: 150, 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, { const useScenarioTable = useTable(getScenarioPage, {
...tableConfig, ...tableConfig,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,12 @@
<div class="p-[16px]"> <div class="p-[16px]">
<!-- TODO:第一版没有模板 --> <!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> --> <!-- <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:第一版先不做依赖 --> <!-- TODO:第一版先不做依赖 -->
<!-- <div class="mb-[8px] flex items-center"> <!-- <div class="mb-[8px] flex items-center">
<div class="text-[var(--color-text-2)]"> <div class="text-[var(--color-text-2)]">

View File

@ -20,11 +20,19 @@
<environmentSelect v-model:current-env-config="currentEnvConfig" /> <environmentSelect v-model:current-env-config="currentEnvConfig" />
<executeButton <executeButton
ref="executeButtonRef" ref="executeButtonRef"
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
:execute-loading="activeScenarioTab.executeLoading" :execute-loading="activeScenarioTab.executeLoading"
@execute="handleExecute" @execute="handleExecute"
@stop-debug="handleStopExecute" @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') }} {{ t('common.save') }}
</a-button> </a-button>
</div> </div>
@ -118,6 +126,7 @@
} from '@/api/modules/api-test/scenario'; } from '@/api/modules/api-test/scenario';
import { getSocket } from '@/api/modules/project-management/commonScript'; import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useLeaveTabUnSaveCheck from '@/hooks/useLeaveTabUnSaveCheck';
import router from '@/router'; import router from '@/router';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { filterTree, getGenerateId, mapTree } from '@/utils'; import { filterTree, getGenerateId, mapTree } from '@/utils';
@ -458,6 +467,25 @@
scenarioDetail.id = res.id; scenarioDetail.id = res.id;
if (!scenarioDetail.steps) { if (!scenarioDetail.steps) {
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); const index = scenarioTabs.value.findIndex((e) => e.id === activeScenarioTab.value.id);
if (index !== -1) { 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 hasLocalExec = computed(() => executeButtonRef.value?.hasLocalExec);
const localExecuteUrl = computed(() => executeButtonRef.value?.localExecuteUrl); const localExecuteUrl = computed(() => executeButtonRef.value?.localExecuteUrl);
const isPriorityLocalExec = computed(() => executeButtonRef.value?.isPriorityLocalExec); const isPriorityLocalExec = computed(() => executeButtonRef.value?.isPriorityLocalExec);

View File

@ -87,7 +87,7 @@
import { GetLoginLogoUrl } from '@/api/requrls/setting/config'; import { GetLoginLogoUrl } from '@/api/requrls/setting/config';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useLoading from '@/hooks/useLoading'; 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 { useAppStore, useUserStore } from '@/store';
import { encrypted } from '@/utils'; import { encrypted } from '@/utils';
import { setLoginExpires } from '@/utils/auth'; import { setLoginExpires } from '@/utils/auth';
@ -160,8 +160,18 @@
const { username, password } = values; const { username, password } = values;
loginConfig.value.username = rememberPassword ? username : ''; loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : ''; 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 { 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 currentRouteName = getFirstRouteNameByPermission(router.getRoutes());
const res = await getProjectInfo(appStore.currentProjectId); const res = await getProjectInfo(appStore.currentProjectId);
if (!res || res.deleted) { if (!res || res.deleted) {

View File

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

View File

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

View File

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