feat(接口管理): 用例编辑&重构用例详情

This commit is contained in:
teukkk 2024-03-17 17:29:19 +08:00 committed by Craftsman
parent f5dc90ffd8
commit 88a4e8b1e3
15 changed files with 624 additions and 241 deletions

View File

@ -55,6 +55,7 @@ import {
SortCaseUrl, SortCaseUrl,
SortDefinitionUrl, SortDefinitionUrl,
SwitchDefinitionScheduleUrl, SwitchDefinitionScheduleUrl,
ToggleFollowCaseUrl,
ToggleFollowDefinitionUrl, ToggleFollowDefinitionUrl,
TransferFileCaseUrl, TransferFileCaseUrl,
TransferFileModuleOptionCaseUrl, TransferFileModuleOptionCaseUrl,
@ -401,6 +402,11 @@ export function getCaseDetail(id: string) {
return MSR.get<ApiCaseDetail>({ url: GetCaseDetailUrl, params: id }); return MSR.get<ApiCaseDetail>({ url: GetCaseDetailUrl, params: id });
} }
// 关注/取消关注接口用例
export function toggleFollowCase(id: string | number) {
return MSR.get({ url: ToggleFollowCaseUrl, params: id });
}
/** /**
* *
*/ */

View File

@ -73,6 +73,7 @@ export const ExecuteCaseUrl = '/api/case/run/'; // 单独执行接口用例
export const GetExecuteHistoryUrl = 'api/case/execute/page'; // 获取用的执行历史 export const GetExecuteHistoryUrl = 'api/case/execute/page'; // 获取用的执行历史
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系 export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系 export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注
/** /**
* *

View File

@ -373,7 +373,7 @@ export interface ExecutePluginRequestParams {
// 执行接口调试入参 // 执行接口调试入参
export interface ExecuteRequestParams { export interface ExecuteRequestParams {
id?: string; id?: string;
reportId: string; reportId?: string;
environmentId: string; environmentId: string;
uploadFileIds: string[]; uploadFileIds: string[];
linkFileIds: string[]; linkFileIds: string[];

View File

@ -343,8 +343,10 @@ export interface AddApiCaseParams extends ExecuteRequestParams {
name: string; name: string;
priority: string; priority: string;
status: string; status: string;
apiDefinitionId: string | number; apiDefinitionId?: string | number;
tags: string[]; tags: string[];
deleteFileIds?: string[];
unLinkFileIds?: string[];
} }
export interface ApiRunModeRequest { export interface ApiRunModeRequest {

View File

@ -139,7 +139,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="request-params-tab px-[16px]"> <div class="px-[16px]">
<MsTab <MsTab
v-model:active-key="requestVModel.activeTab" v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList" :content-tab-list="contentTabList"
@ -1528,9 +1528,6 @@
:deep(.arco-tabs-tab) { :deep(.arco-tabs-tab) {
@apply leading-none; @apply leading-none;
} }
.request-params-tab :deep(.arco-tabs-nav-tab) {
border-bottom: 1px solid var(--color-text-n8) !important;
}
.hidden-second { .hidden-second {
:deep(.arco-split-trigger) { :deep(.arco-split-trigger) {
@apply hidden; @apply hidden;

View File

@ -338,6 +338,7 @@
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{ const props = defineProps<{
isCase?: boolean; // case
detail: RequestParam; detail: RequestParam;
protocols: ProtocolItem[]; protocols: ProtocolItem[];
}>(); }>();
@ -409,6 +410,12 @@
} }
} }
watchEffect(() => {
if (!props.isCase) return;
// case
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
});
watch( watch(
() => props.detail.id, () => props.detail.id,
() => { () => {

View File

@ -2,19 +2,6 @@
<div class="h-full w-full overflow-hidden"> <div class="h-full w-full overflow-hidden">
<div class="px-[18px] pt-[16px]"> <div class="px-[18px] pt-[16px]">
<MsDetailCard <MsDetailCard
v-if="props.isCaseDetail"
:title="`【${previewDetail.num}】${previewDetail.name}`"
:description="description"
>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
<template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" />
</template>
</MsDetailCard>
<MsDetailCard
v-else
:title="`【${previewDetail.num}】${previewDetail.name}`" :title="`【${previewDetail.num}】${previewDetail.name}`"
:description="description" :description="description"
:simple-show-count="4" :simple-show-count="4"
@ -78,8 +65,6 @@
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue'; import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import detailTab from './detail.vue'; import detailTab from './detail.vue';
import history from './history.vue'; import history from './history.vue';
import quote from './quote.vue'; import quote from './quote.vue';
@ -100,7 +85,6 @@
detail: RequestParam; detail: RequestParam;
moduleTree: ModuleTreeNode[]; moduleTree: ModuleTreeNode[];
protocols: ProtocolItem[]; protocols: ProtocolItem[];
isCaseDetail?: boolean; //
}>(); }>();
const emit = defineEmits(['updateFollow']); const emit = defineEmits(['updateFollow']);
@ -113,7 +97,6 @@
() => props.detail.id, () => props.detail.id,
() => { () => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
if (props.isCaseDetail) return;
const tableParam = getValidRequestTableParams(previewDetail.value); // props.detail const tableParam = getValidRequestTableParams(previewDetail.value); // props.detail
previewDetail.value = { previewDetail.value = {
...previewDetail.value, ...previewDetail.value,
@ -137,66 +120,49 @@
} }
); );
const description = computed(() => { const description = computed(() => [
const commonDescription = [ {
{ key: 'type',
key: 'type', locale: 'apiTestManagement.apiType',
locale: 'apiTestManagement.apiType', value: previewDetail.value.method,
value: previewDetail.value.method, },
}, {
{ key: 'path',
key: 'path', locale: 'apiTestManagement.path',
locale: 'apiTestManagement.path', value: previewDetail.value.url || previewDetail.value.path,
value: previewDetail.value.url || previewDetail.value.path, },
}, {
{ key: 'tags',
key: 'tags', locale: 'common.tag',
locale: 'common.tag', value: previewDetail.value.tags,
value: previewDetail.value.tags, },
}, {
]; key: 'description',
if (!props.isCaseDetail) { locale: 'common.desc',
return [ value: previewDetail.value.description,
...commonDescription, width: '100%',
...[ },
{ {
key: 'description', key: 'belongModule',
locale: 'common.desc', locale: 'apiTestManagement.belongModule',
value: previewDetail.value.description, value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path,
width: '100%', },
}, {
{ key: 'creator',
key: 'belongModule', locale: 'common.creator',
locale: 'apiTestManagement.belongModule', value: previewDetail.value.createUserName,
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path, },
}, {
{ key: 'createTime',
key: 'creator', locale: 'apiTestManagement.createTime',
locale: 'common.creator', value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
value: previewDetail.value.createUserName, },
}, {
{ key: 'updateTime',
key: 'createTime', locale: 'apiTestManagement.updateTime',
locale: 'apiTestManagement.createTime', value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'), },
}, ]);
{
key: 'updateTime',
locale: 'apiTestManagement.updateTime',
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
],
];
}
//
const caseDescription = commonDescription.slice();
caseDescription.splice(1, 0, {
key: 'priority',
locale: 'case.caseLevel',
value: previewDetail.value.priority,
});
return caseDescription;
});
const followLoading = ref(false); const followLoading = ref(false);
async function toggleFollowReview() { async function toggleFollowReview() {

View File

@ -1,33 +1,126 @@
<template> <template>
<preview <div class="h-full w-full overflow-hidden">
:detail="activeApiTab" <a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load>
:module-tree="props.moduleTree" <template #extra>
:protocols="protocols" <div v-show="!props.isDrawer" class="flex gap-[12px]">
is-case-detail <a-button type="primary">
@update-follow="activeApiTab.follow = !activeApiTab.follow" {{ t('apiTestManagement.execute') }}
/> </a-button>
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
<a-button>{{ t('common.operation') }}</a-button>
<template #content>
<a-doption value="edit">
<MsIcon type="icon-icon_edit_outlined" />
{{ t('common.edit') }}
</a-doption>
<a-doption value="share">
<MsIcon type="icon-icon_share1" />
{{ t('common.share') }}
</a-doption>
<a-doption value="fork">
<MsIcon
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`"
/>
{{ t('common.fork') }}
</a-doption>
<a-divider margin="4px" />
<a-doption class="error-6 text-[rgb(var(--danger-6))]">
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown>
</div>
</template>
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]">
<MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]">
<template #titleAppend>
<a-button v-show="props.isDrawer" type="primary" size="mini">
{{ t('apiTestManagement.execute') }}
</a-button>
</template>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
<template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" />
</template>
</MsDetailCard>
<detailTab :detail="caseDetail" :protocols="protocols" is-case />
</a-tab-pane>
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
<quote :source-id="caseDetail.id" />
</a-tab-pane>
<!-- <a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
</a-tab-pane> -->
<a-tab-pane key="changeHistory" :title="t('apiTestManagement.changeHistory')" class="px-[18px] py-[16px]">
<history :source-id="caseDetail.id" />
</a-tab-pane>
</a-tabs>
</div>
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" :protocol="props.protocol" v-bind="$attrs" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import preview from '@/views/api-test/management/components/management/api/preview/index.vue'; import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import detailTab from '../api/preview/detail.vue';
import history from '../api/preview/history.vue';
import quote from '../api/preview/quote.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { getProtocolList } from '@/api/modules/api-test/common'; import { getProtocolList } from '@/api/modules/api-test/common';
import { toggleFollowCase } from '@/api/modules/api-test/management';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common'; import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common'; import { RequestMethods } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{ const props = defineProps<{
moduleTree: ModuleTreeNode[]; // isDrawer?: boolean; //
detail: RequestParam;
protocol: string;
}>(); }>();
const emit = defineEmits(['updateFollow']);
const { copy, isSupported } = useClipboard();
const { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
const activeApiTab = defineModel<RequestParam>('activeApiTab', { const caseDetail = computed<RequestParam>(() => cloneDeep(props.detail)); // props.detailprops.detail
required: true, const activeKey = ref('detail');
});
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: caseDetail.value.method,
},
{
key: 'priority',
locale: 'case.caseLevel',
value: caseDetail.value.priority,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: caseDetail.value.url || caseDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: caseDetail.value.tags,
},
]);
const protocols = ref<ProtocolItem[]>([]); const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() { async function initProtocolList() {
@ -42,4 +135,81 @@
onBeforeMount(() => { onBeforeMount(() => {
initProtocolList(); initProtocolList();
}); });
const followLoading = ref(false);
async function follow() {
try {
followLoading.value = true;
await toggleFollowCase(caseDetail.value.id);
Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
emit('updateFollow');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
followLoading.value = false;
}
}
function share() {
if (isSupported) {
copy(`${window.location.href}&dId=${caseDetail.value.id}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
function editCase() {
createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value, false);
}
function handleSelect(val: string | number | Record<string, any> | undefined) {
switch (val) {
case 'edit':
editCase();
break;
case 'share':
share();
break;
case 'fork':
follow();
break;
default:
break;
}
}
defineExpose({
editCase,
share,
follow,
});
</script> </script>
<style lang="less" scoped>
:deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-text-n8);
}
:deep(.arco-tabs-content) {
@apply pt-0;
.arco-tabs-content-item {
@apply px-0;
}
}
:deep(.ms-detail-card-desc) {
gap: 16px;
flex-wrap: nowrap !important;
& > div:nth-of-type(n) {
width: auto;
max-width: 30%;
}
}
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
unmount-on-close
:title="t('caseManagement.featureCase.caseDetail')"
:width="894"
:footer="false"
no-content-padding
>
<template #headerLeft>
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
</template>
<template #tbutton>
<div class="flex items-center gap-[4px]">
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
type="icon"
status="secondary"
@click="caseDerailRef?.editCase()"
>
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }}
</MsButton>
<MsButton type="icon" status="secondary" @click="caseDerailRef?.share()">
<MsIcon type="icon-icon_share1" class="mr-[8px]" />
{{ t('common.share') }}
</MsButton>
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
type="icon"
status="secondary"
@click="caseDerailRef?.follow()"
>
<MsIcon
:type="props.detail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="mr-[8px]"
:class="[props.detail.follow ? 'text-[rgb(var(--warning-6))]' : '']"
/>
{{ t('common.fork') }}
</MsButton>
<MsButton type="icon" status="secondary">
<a-dropdown position="br">
<div>
<icon-more class="mr-[8px]" />
<span> {{ t('common.more') }}</span>
</div>
<template #content>
<a-doption
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
class="error-6 text-[rgb(var(--danger-6))]"
>
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown>
</MsButton>
</div>
</template>
<caseDetail
ref="caseDerailRef"
is-drawer
:detail="props.detail"
:protocol="props.protocol"
:api-detail="props.apiDetail"
v-bind="$attrs"
/>
</MsDrawer>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import environmentSelect from '../../environmentSelect.vue';
import caseDetail from './caseDetail.vue';
import { useI18n } from '@/hooks/useI18n';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{
detail: RequestParam;
protocol: string;
apiDetail: RequestParam;
}>();
const { t } = useI18n();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const caseDerailRef = ref<InstanceType<typeof caseDetail>>();
</script>
<style scoped lang="less">
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="overflow-hidden p-[16px_22px]"> <div class="overflow-hidden p-[16px_22px]">
<div class="mb-[16px] flex items-center justify-between"> <div :class="['mb-[16px]', 'flex', 'items-center', props.isApi ? 'justify-between' : 'justify-end']">
<a-button <a-button
v-show="props.isApi" v-show="props.isApi"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']" v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
@ -37,7 +37,9 @@
@drag-change="handleDragChange" @drag-change="handleDragChange"
> >
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="openCaseTab(record)">{{ record.num }}</MsButton> <MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">{{
record.num
}}</MsButton>
</template> </template>
<template #caseLevel="{ record }"> <template #caseLevel="{ record }">
<a-select <a-select
@ -236,12 +238,19 @@
</template> </template>
</a-modal> </a-modal>
<createAndEditCaseDrawer <createAndEditCaseDrawer
v-if="props.isApi"
ref="createAndEditCaseDrawerRef" ref="createAndEditCaseDrawerRef"
:protocol="props.protocol" :protocol="props.protocol"
:api-detail="apiDetail as RequestParam" :api-detail="apiDetail"
@load-case="loadCaseListAndResetSelector()" @load-case="loadCaseListAndResetSelector()"
/> />
<caseDetailDrawer
v-model:visible="caseDetailDrawerVisible"
:detail="caseDetail as RequestParam"
:protocol="props.protocol"
:api-detail="apiDetail as RequestParam"
@update-follow="caseDetail.follow = !caseDetail.follow"
@load-case="(id: string) => loadCase(id)"
/>
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480"> <a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<template #title> <template #title>
{{ t('report.trigger.batch.execution') }} {{ t('report.trigger.batch.execution') }}
@ -337,8 +346,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
@ -346,6 +357,7 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import caseDetailDrawer from './caseDetailDrawer.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue'; import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -356,6 +368,7 @@
deleteCase, deleteCase,
dragSort, dragSort,
executeCase, executeCase,
getCaseDetail,
getCasePage, getCasePage,
getEnvList, getEnvList,
getPoolId, getPoolId,
@ -376,6 +389,7 @@
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
const props = defineProps<{ const props = defineProps<{
isApi: boolean; // case tab isApi: boolean; // case tab
@ -949,15 +963,52 @@
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>(); const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
function createCase() { function createCase() {
createAndEditCaseDrawerRef.value?.open(); createAndEditCaseDrawerRef.value?.open(props.apiDetail?.id as string);
} }
function copyCase(record: ApiCaseDetail) { function copyCase(record: ApiCaseDetail) {
createAndEditCaseDrawerRef.value?.open(record, true); createAndEditCaseDrawerRef.value?.open(record.apiDefinitionId, record, true);
} }
function openCaseTab(record: ApiCaseDetail) { function openCaseTab(record: ApiCaseDetail) {
emit('openCaseTab', record); emit('openCaseTab', record);
} }
const caseDetailDrawerVisible = ref(false);
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const caseDetail = ref<Record<string, any>>({});
async function getCaseDetailInfo(id: string) {
try {
const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ;
// if (res.protocol === 'HTTP') { // TODO: protocol
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// }
caseDetail.value = {
...cloneDeep(defaultCaseParams as RequestParam),
...({
...res.request,
...res,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path,
...parseRequestBodyResult,
} as Partial<TabItem>),
};
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function openCaseDetailDrawer(id: string) {
await getCaseDetailInfo(id);
caseDetailDrawerVisible.value = true;
}
// api
async function loadCase(id: string) {
getCaseDetailInfo(id);
loadCaseList();
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,14 +1,14 @@
<template> <template>
<MsDrawer <MsDrawer
v-model:visible="innerVisible" v-model:visible="innerVisible"
:title="t('case.createCase')" :title="isEdit ? t('case.updateCase') : t('case.createCase')"
:width="894" :width="894"
no-content-padding no-content-padding
:ok-text="t('common.create')" :ok-text="isEdit ? 'common.update' : 'common.create'"
:ok-loading="drawerLoading" :ok-loading="drawerLoading"
:save-continue-text="t('case.saveContinueText')" :save-continue-text="t('case.saveContinueText')"
:show-continue="true" :show-continue="!isEdit && !!props.apiDetail"
@confirm="handleDrawerConfirm" @confirm="handleDrawerConfirm(false)"
@continue="handleDrawerConfirm(true)" @continue="handleDrawerConfirm(true)"
@cancel="handleSaveCaseCancel" @cancel="handleSaveCaseCancel"
> >
@ -18,7 +18,7 @@
<div class="flex h-full flex-col overflow-hidden"> <div class="flex h-full flex-col overflow-hidden">
<div class="px-[16px] pt-[16px]"> <div class="px-[16px] pt-[16px]">
<MsDetailCard <MsDetailCard
:title="`【${apiDataDetail.num}】${apiDataDetail.name}`" :title="`【${apiDetailInfo.num}】${apiDetailInfo.name}`"
:description="description" :description="description"
class="!flex-row justify-between" class="!flex-row justify-between"
> >
@ -26,11 +26,11 @@
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag /> <apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template> </template>
</MsDetailCard> </MsDetailCard>
<a-form ref="formRef" class="mt-[16px]" :model="caseModalForm" layout="vertical"> <a-form ref="formRef" class="mt-[16px]" :model="detailForm" layout="vertical">
<a-form-item field="name" label="" :rules="[{ required: true, message: t('case.caseNameRequired') }]"> <a-form-item field="name" label="" :rules="[{ required: true, message: t('case.caseNameRequired') }]">
<div class="flex w-full items-center gap-[8px]"> <div class="flex w-full items-center gap-[8px]">
<a-input <a-input
v-model:model-value="caseModalForm.name" v-model:model-value="detailForm.name"
:placeholder="t('case.caseNamePlaceholder')" :placeholder="t('case.caseNamePlaceholder')"
allow-clear allow-clear
:max-length="255" :max-length="255"
@ -43,9 +43,9 @@
</a-form-item> </a-form-item>
<div class="flex gap-[16px]"> <div class="flex gap-[16px]">
<a-form-item field="priority" :label="t('case.caseLevel')"> <a-form-item field="priority" :label="t('case.caseLevel')">
<a-select v-model:model-value="caseModalForm.priority" :placeholder="t('common.pleaseSelect')"> <a-select v-model:model-value="detailForm.priority" :placeholder="t('common.pleaseSelect')">
<template #label> <template #label>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="caseModalForm.priority" /></span> <span class="text-[var(--color-text-2)]"> <caseLevel :case-level="detailForm.priority" /></span>
</template> </template>
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value"> <a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
<caseLevel :case-level="item.label as CaseLevel" /> <caseLevel :case-level="item.label as CaseLevel" />
@ -53,9 +53,9 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item field="status" :label="t('apiTestManagement.apiStatus')"> <a-form-item field="status" :label="t('apiTestManagement.apiStatus')">
<a-select v-model:model-value="caseModalForm.status" :placeholder="t('common.pleaseSelect')"> <a-select v-model:model-value="detailForm.status" :placeholder="t('common.pleaseSelect')">
<template #label> <template #label>
<apiStatus :status="caseModalForm.status" /> <apiStatus :status="detailForm.status" />
</template> </template>
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item"> <a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
<apiStatus :status="item" /> <apiStatus :status="item" />
@ -63,7 +63,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="caseModalForm.tags" /> <MsTagsInput v-model:model-value="detailForm.tags" />
</a-form-item> </a-form-item>
</div> </div>
</a-form> </a-form>
@ -72,11 +72,11 @@
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<requestComposition <requestComposition
ref="requestCompositionRef" ref="requestCompositionRef"
v-model:request="apiDataDetail" v-model:request="detailForm"
:is-case="true" :is-case="true"
hide-response-layout-switch hide-response-layout-switch
:upload-temp-file-api="uploadTempFileCase" :upload-temp-file-api="uploadTempFileCase"
:file-save-as-source-id="apiDataDetail.id" :file-save-as-source-id="detailForm.id"
:file-module-options-api="getTransferOptionsCase" :file-module-options-api="getTransferOptionsCase"
:file-save-as-api="transferFileCase" :file-save-as-api="transferFileCase"
:current-env-config="currentEnvConfig" :current-env-config="currentEnvConfig"
@ -103,41 +103,55 @@
import { import {
addCase, addCase,
getDefinitionDetail,
getTransferOptionsCase, getTransferOptionsCase,
transferFileCase, transferFileCase,
updateCase,
uploadTempFileCase, uploadTempFileCase,
} from '@/api/modules/api-test/management'; } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ApiCaseDetail } from '@/models/apiTest/management'; import { AddApiCaseParams, ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { EnvConfig } from '@/models/projectManagement/environmental'; import { EnvConfig } from '@/models/projectManagement/environmental';
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum'; import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { casePriorityOptions } from '@/views/api-test/components/config'; import { casePriorityOptions } from '@/views/api-test/components/config';
const props = defineProps<{ const props = defineProps<{
apiDetail: RequestParam; apiDetail?: RequestParam | ApiDefinitionDetail;
}>();
const emit = defineEmits<{
(e: 'loadCase', id?: string): void;
}>(); }>();
const emit = defineEmits(['loadCase']);
const apiDataDetail = ref<RequestParam>(cloneDeep(props.apiDetail));
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore();
const innerVisible = ref(false); const innerVisible = ref(false);
const drawerLoading = ref(false); const drawerLoading = ref(false);
const apiDefinitionId = ref('');
const apiDetailInfo = ref<Record<string, any>>({});
async function getApiDetail() {
try {
apiDetailInfo.value = await getDefinitionDetail(apiDefinitionId.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const description = computed(() => [ const description = computed(() => [
{ {
key: 'type', key: 'type',
locale: 'apiTestManagement.apiType', locale: 'apiTestManagement.apiType',
value: apiDataDetail.value.method, value: apiDetailInfo.value.method,
}, },
{ {
key: 'path', key: 'path',
locale: 'apiTestManagement.path', locale: 'apiTestManagement.path',
value: apiDataDetail.value.url || apiDataDetail.value.path, value: apiDetailInfo.value.url || apiDetailInfo.value.path,
}, },
]); ]);
@ -145,47 +159,84 @@
const currentEnvConfig = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig); const currentEnvConfig = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const initForm: any = {
apiDefinitionId: apiDataDetail.value.id as string,
name: '',
priority: 'P0',
tags: [],
status: RequestDefinitionStatus.PROCESSING,
};
const caseModalForm = ref({ ...initForm });
const requestCompositionRef = ref<InstanceType<typeof requestComposition>>(); const requestCompositionRef = ref<InstanceType<typeof requestComposition>>();
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const defaultDetail: RequestParam = {
apiDefinitionId: apiDefinitionId.value,
...(defaultCaseParams as RequestParam),
};
const detailForm = ref(cloneDeep(defaultDetail));
const isEdit = ref(false);
function open(record?: ApiCaseDetail, isCopy?: boolean) { function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
innerVisible.value = true; apiDefinitionId.value = apiId;
if (isCopy) { // apiapicaseapi
caseModalForm.value.name = record?.name; if (props.apiDetail) {
apiDetailInfo.value = props.apiDetail;
} else {
getApiDetail();
} }
//
if (isCopy) {
detailForm.value.name = `copy_${record?.name}`;
}
//
if (!isCopy && record?.id) {
isEdit.value = true;
detailForm.value = cloneDeep(record as RequestParam);
}
innerVisible.value = true;
} }
function handleSaveCaseCancel() { function handleSaveCaseCancel() {
drawerLoading.value = false;
isEdit.value = false;
innerVisible.value = false; innerVisible.value = false;
formRef.value?.resetFields(); formRef.value?.resetFields();
caseModalForm.value = { ...initForm }; detailForm.value = cloneDeep(defaultDetail);
} }
function handleDrawerConfirm(isContinue: boolean) { function handleDrawerConfirm(isContinue: boolean) {
formRef.value?.validate(async (errors) => { formRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
drawerLoading.value = true; drawerLoading.value = true;
const params = { ...requestCompositionRef.value?.makeRequestParams(), ...caseModalForm.value }; //
if (!requestCompositionRef.value?.makeRequestParams()) return;
const { linkFileIds, uploadFileIds, request, unLinkFileIds, deleteFileIds } =
requestCompositionRef.value.makeRequestParams();
const { name, priority, status, tags, id } = detailForm.value;
const params: AddApiCaseParams = {
projectId: appStore.currentProjectId,
environmentId: currentEnvConfig.value?.id as string,
apiDefinitionId: apiDefinitionId.value,
linkFileIds,
uploadFileIds,
request,
id: id as string,
name,
priority,
status,
tags,
unLinkFileIds,
deleteFileIds,
};
try { try {
await addCase(params); if (isEdit.value) {
Message.success(t('common.updateSuccess')); await updateCase(params);
Message.success(t('common.updateSuccess'));
} else {
await addCase(params);
Message.success(t('common.createSuccess'));
}
emit('loadCase', id as string);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
} }
if (!isContinue) { if (!isContinue) {
emit('loadCase');
handleSaveCaseCancel(); handleSaveCaseCancel();
} }
caseModalForm.value = { ...initForm }; detailForm.value = cloneDeep(defaultDetail);
drawerLoading.value = false; drawerLoading.value = false;
} }
}); });

View File

@ -9,7 +9,13 @@
/> />
</div> </div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden"> <div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<caseDetail :active-api-tab="activeApiTab" :module-tree="props.moduleTree" /> <caseDetail
:detail="activeApiTab"
:module-tree="props.moduleTree"
:protocol="props.protocol"
@update-follow="activeApiTab.follow = !activeApiTab.follow"
@load-case="(id: string) => openOrUpdateCaseTab(false, id)"
/>
</div> </div>
</div> </div>
</template> </template>
@ -24,9 +30,7 @@
import { ApiCaseDetail } from '@/models/apiTest/management'; import { ApiCaseDetail } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { RequestAuthType, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils'; import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
@ -47,106 +51,37 @@
required: true, required: true,
}); });
const initDefaultId = `case-${Date.now()}`; const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const defaultCaseParams: RequestParam = {
type: 'case',
id: initDefaultId,
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
protocol: 'HTTP',
tags: [],
description: '',
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //
};
function addTab(defaultProps?: Partial<TabItem>) {
apiTabs.value.push({
...cloneDeep(defaultCaseParams),
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
}
const loading = ref(false); const loading = ref(false);
async function openCaseTab(apiInfo: ApiCaseDetail) { async function openOrUpdateCaseTab(isOpen: boolean, id: string) {
const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
);
if (isLoadedTabIndex > -1) {
// tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
return;
}
try { try {
loading.value = true; loading.value = true;
const res = await getCaseDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id); const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ; const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ;
// if (res.protocol === 'HTTP') { // TODO: protocol // if (res.protocol === 'HTTP') { // TODO: protocol
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id // parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// } // }
addTab({ const tabItemInfo = {
...res.request, ...cloneDeep(defaultCaseParams as RequestParam),
...res, ...({
response: cloneDeep(defaultResponse), ...res.request,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response ...res,
url: res.path, // responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
...parseRequestBodyResult, url: res.path,
}); ...parseRequestBodyResult,
} as Partial<TabItem>),
};
if (isOpen) {
apiTabs.value.push(tabItemInfo);
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
} else {
//
const index = apiTabs.value.findIndex((item) => item.id === id);
apiTabs.value[index] = tabItemInfo;
activeApiTab.value = tabItemInfo;
}
nextTick(() => { nextTick(() => {
loading.value = false; // loading loading.value = false; // loading
}); });
@ -156,4 +91,16 @@
loading.value = false; loading.value = false;
} }
} }
async function openCaseTab(apiInfo: ApiCaseDetail) {
const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
);
if (isLoadedTabIndex > -1) {
// tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
return;
}
await openOrUpdateCaseTab(true, typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
}
</script> </script>

View File

@ -58,6 +58,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue'; import { SelectOptionData } from '@arco-design/web-vue';
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';
import api from './api/index.vue'; import api from './api/index.vue';
@ -73,8 +74,17 @@
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental'; import { EnvConfig } from '@/models/projectManagement/environmental';
import {
RequestAuthType,
RequestComposition,
RequestDefinitionStatus,
RequestMethods,
ResponseComposition,
} from '@/enums/apiEnum';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum'; import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
const props = defineProps<{ const props = defineProps<{
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
@ -113,6 +123,76 @@
]); ]);
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam); const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
// apidefaultCaseParams
const initDefaultId = `case-${Date.now()}`;
const defaultCaseParams: RequestParam = {
id: initDefaultId,
type: 'case',
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
protocol: 'HTTP',
tags: [],
description: '',
priority: 'P0',
status: RequestDefinitionStatus.PROCESSING,
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //
};
// id // id
watch( watch(
() => props.activeModule, () => props.activeModule,
@ -236,6 +316,7 @@
/** 向孙组件提供属性 */ /** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig)); provide('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams));
defineExpose({ defineExpose({
newTab, newTab,

View File

@ -191,6 +191,7 @@ export default {
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.', 'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
'case.recycle.confirmRecovery': 'Confirm recovery', 'case.recycle.confirmRecovery': 'Confirm recovery',
'case.createCase': 'Create Case', 'case.createCase': 'Create Case',
'case.updateCase': 'Update Case',
'case.saveContinueText': 'Save & continue', 'case.saveContinueText': 'Save & continue',
'case.detail.changeHistoryTip': `View and compare historical changes. According to the administrator's setting rules, historical changes will be automatically deleted`, 'case.detail.changeHistoryTip': `View and compare historical changes. According to the administrator's setting rules, historical changes will be automatically deleted`,
'case.detail.noReminders': 'No longer remind', 'case.detail.noReminders': 'No longer remind',

View File

@ -183,6 +183,7 @@ export default {
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api', 'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
'case.recycle.confirmRecovery': '确认恢复', 'case.recycle.confirmRecovery': '确认恢复',
'case.createCase': '创建用例', 'case.createCase': '创建用例',
'case.updateCase': '更新用例',
'case.saveContinueText': '保存并继续创建', 'case.saveContinueText': '保存并继续创建',
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除', 'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'case.detail.noReminders': '不再提醒', 'case.detail.noReminders': '不再提醒',
@ -191,5 +192,4 @@ export default {
'case.detail.operator': '操作人', 'case.detail.operator': '操作人',
'case.detail.tableColumnUpdateTime': '更新时间', 'case.detail.tableColumnUpdateTime': '更新时间',
'case.detail.execute.success': '执行成功', 'case.detail.execute.success': '执行成功',
}; };