feat(功能用例): 脑图加载用例&接口部分 bug 修复

This commit is contained in:
baiqi 2024-05-20 20:35:03 +08:00 committed by Craftsman
parent 98a580a427
commit d9ca6aabaa
24 changed files with 524 additions and 359 deletions

View File

@ -9,7 +9,7 @@ import type { ErrorMessageMode } from '#/axios';
export default function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = 'message'): void {
const { t } = useI18n();
const { logout, isLoginPage } = useUser();
const { logout, isLoginPage, isWhiteListPage } = useUser();
let errMessage = '';
switch (status) {
case 400:
@ -17,7 +17,7 @@ export default function checkStatus(status: number, msg: string, errorMessageMod
break;
case 401: {
errMessage = msg || t('api.errMsg401');
if (!isLoginPage()) {
if (!isLoginPage() && !isWhiteListPage()) {
// 不是登录页再调用logout
logout();
}

View File

@ -1,3 +1,4 @@
import type { MinderJsonNode } from '@/components/pure/ms-minder-editor/props';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import MSR from '@/api/http/index';
@ -187,7 +188,7 @@ export function saveCaseMinder(data: FeatureCaseMinder) {
// 获取脑图
export function getCaseMinder(data: { projectId: string; moduleId: string }) {
return MSR.post({ url: `${GetCaseMinderUrl}`, data });
return MSR.post<MinderJsonNode[]>({ url: `${GetCaseMinderUrl}`, data });
}
// 回收站

View File

@ -43,6 +43,12 @@
<paramTable
v-model:params="innerParams"
:columns="columns"
:default-param-item="{
key: '',
value: '',
description: '',
required: false,
}"
:scroll="{ x: 'auto' }"
:height-used="heightUsed"
:selectable="false"

View File

@ -1,7 +1,9 @@
<template>
<MsMinderEditor
v-model:activeExtraKey="activeExtraKey"
:tags="tags"
v-model:extra-visible="extraVisible"
v-model:loading="loading"
:tags="[]"
:import-json="importJson"
:replaceable-tags="replaceableTags"
:insert-node="insertNode"
@ -15,8 +17,8 @@
@save="handleMinderSave"
>
<template #extractTabContent>
<div>
<div v-if="activeExtraKey === 'baseInfo'" class="pl-[16px]">
<div v-if="activeExtraKey === 'baseInfo'" class="h-full pl-[16px]">
<div class="baseInfo-form">
<a-skeleton v-if="baseInfoLoading" :loading="baseInfoLoading" :animation="true">
<a-space direction="vertical" class="w-full" size="large">
<a-skeleton-line :rows="rowLength" :line-height="30" :line-spacing="30" />
@ -30,35 +32,6 @@
>
<a-input v-model:model-value="baseInfoForm.name" :placeholder="t('common.pleaseInput')"></a-input>
</a-form-item>
<a-form-item
field="moduleId"
asterisk-position="end"
:label="t('caseManagement.featureCase.ModuleOwned')"
:rules="[{ required: true, message: t('system.orgTemplate.moduleRuleTip') }]"
>
<a-tree-select
v-model="baseInfoForm.moduleId"
:allow-search="true"
:data="caseTree"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:draggable="false"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[300px] text-[var(--color-text-1)]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
</a-form-item>
<MsFormCreate
v-if="formRules.length"
ref="formCreateRef"
@ -70,176 +43,171 @@
<MsTagsInput v-model:model-value="baseInfoForm.tags" :max-tag-count="6" />
</a-form-item>
</a-form>
<div class="flex items-center gap-[12px]">
<a-button type="primary" @click="handleSave">{{ t('common.save') }}</a-button>
<a-button type="secondary">{{ t('common.cancel') }}</a-button>
</div>
</div>
<div v-else-if="activeExtraKey === 'attachment'" class="pl-[16px]">
<MsAddAttachment
v-model:file-list="fileList"
multiple
only-button
@change="handleFileChange"
@link-file="() => (showLinkFileDrawer = true)"
/>
<MsFileList
v-if="fileList.length > 0"
ref="fileListRef"
v-model:file-list="fileList"
mode="static"
:init-file-save-tips="t('ms.upload.waiting_save')"
:show-upload-type-desc="true"
>
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init' && item.file.type.includes('image/')"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<SaveAsFilePopover
v-model:visible="transferVisible"
:saving-file="activeTransferFileParams"
:file-save-as-source-id="activeCase.id"
:file-save-as-api="transferFileRequest"
:file-module-options-api="getTransferFileTree"
source-id-key="caseId"
/>
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="transferFile(item)"
>
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton
v-if="item.file.type.includes('/image')"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton
v-if="activeCase.id"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
<MsButton
v-if="activeCase.id && item.isUpdateFlag"
type="button"
status="primary"
@click="handleUpdateFile(item)"
>
{{ t('common.update') }}
</MsButton>
</div>
</template>
<template #title="{ item }">
<span v-if="item.isUpdateFlag" class="ml-4 flex items-center font-normal text-[rgb(var(--warning-6))]">
<icon-exclamation-circle-fill />
<span>{{ t('caseManagement.featureCase.fileIsUpdated') }}</span>
</span>
</template>
</MsFileList>
<div class="flex items-center gap-[12px] bg-white py-[16px]">
<a-button type="primary" @click="handleSave">{{ t('common.save') }}</a-button>
<a-button type="secondary">{{ t('common.cancel') }}</a-button>
</div>
<div v-else-if="activeExtraKey === 'comments'" class="pl-[16px]">
<div class="flex items-center justify-between">
<div class="text-[var(--color-text-4)]">
{{
t('ms.minders.commentTotal', {
num: activeComment === 'caseComment' ? commentList.length : reviewCommentList.length,
})
}}
</div>
<div v-else-if="activeExtraKey === 'attachment'" class="pl-[16px]">
<MsAddAttachment
v-model:file-list="fileList"
multiple
only-button
@change="handleFileChange"
@link-file="() => (showLinkFileDrawer = true)"
/>
<MsFileList
v-if="fileList.length > 0"
ref="fileListRef"
v-model:file-list="fileList"
mode="static"
:init-file-save-tips="t('ms.upload.waiting_save')"
:show-upload-type-desc="true"
>
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init' && item.file.type.includes('image/')"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<SaveAsFilePopover
v-model:visible="transferVisible"
:saving-file="activeTransferFileParams"
:file-save-as-source-id="activeCase.id"
:file-save-as-api="transferFileRequest"
:file-module-options-api="getTransferFileTree"
source-id-key="caseId"
/>
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="transferFile(item)"
>
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton
v-if="item.file.type.includes('/image')"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton
v-if="activeCase.id"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
<MsButton
v-if="activeCase.id && item.isUpdateFlag"
type="button"
status="primary"
@click="handleUpdateFile(item)"
>
{{ t('common.update') }}
</MsButton>
</div>
<a-select
v-model:model-value="activeComment"
:options="commentTypeOptions"
class="w-[120px]"
@change="getAllCommentList"
></a-select>
</div>
<ReviewCommentList
v-if="activeComment === 'reviewComment' || activeComment === 'executiveComment'"
:review-comment-list="reviewCommentList"
:active-comment="activeComment"
/>
<template v-else>
<MsComment
:upload-image="handleUploadImage"
:comment-list="commentList"
:preview-url="PreviewEditorImageUrl"
@delete="handleDelete"
@update-or-add="handleUpdateOrAdd"
/>
<MsEmpty v-if="commentList.length === 0" />
</template>
<inputComment
ref="commentInputRef"
v-model:content="content"
v-model:notice-user-ids="noticeUserIds"
v-permission="['FUNCTIONAL_CASE:READ+COMMENT']"
:preview-url="PreviewEditorImageUrl"
:is-active="isActive"
mode="textarea"
is-show-avatar
is-use-bottom
:upload-image="handleUploadImage"
@publish="publishHandler"
@cancel="cancelPublish"
/>
</div>
<div v-else class="pl-[16px]">
<a-button
v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])"
class="mr-3"
type="primary"
@click="linkBug"
>
{{ t('caseManagement.featureCase.linkDefect') }}
</a-button>
<a-button v-permission="['PROJECT_BUG:READ+ADD']" type="outline" @click="createBug"
>{{ t('caseManagement.featureCase.createDefect') }}
</a-button>
<div class="bug-list">
<div v-for="item of bugList" :key="item.id" class="bug-item">
<div class="mb-[4px] flex items-center justify-between">
<MsButton type="text" @click="goBug(item.id)">{{ item.num }}</MsButton>
<MsButton type="text" @click="disassociateBug(item.id)">
{{ t('ms.add.attachment.cancelAssociate') }}
</MsButton>
</div>
<a-tooltip :content="item.name">
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div>
<MsEmpty v-if="bugList.length === 0" />
<template #title="{ item }">
<span v-if="item.isUpdateFlag" class="ml-4 flex items-center font-normal text-[rgb(var(--warning-6))]">
<icon-exclamation-circle-fill />
<span>{{ t('caseManagement.featureCase.fileIsUpdated') }}</span>
</span>
</template>
</MsFileList>
</div>
<div v-else-if="activeExtraKey === 'comments'" class="pl-[16px]">
<div class="flex items-center justify-between">
<div class="text-[var(--color-text-4)]">
{{
t('ms.minders.commentTotal', {
num: activeComment === 'caseComment' ? commentList.length : reviewCommentList.length,
})
}}
</div>
<a-select
v-model:model-value="activeComment"
:options="commentTypeOptions"
class="w-[120px]"
@change="getAllCommentList"
></a-select>
</div>
<ReviewCommentList
v-if="activeComment === 'reviewComment' || activeComment === 'executiveComment'"
:review-comment-list="reviewCommentList"
:active-comment="activeComment"
/>
<template v-else>
<MsComment
:upload-image="handleUploadImage"
:comment-list="commentList"
:preview-url="PreviewEditorImageUrl"
@delete="handleDelete"
@update-or-add="handleUpdateOrAdd"
/>
<MsEmpty v-if="commentList.length === 0" />
</template>
<inputComment
ref="commentInputRef"
v-model:content="content"
v-model:notice-user-ids="noticeUserIds"
v-permission="['FUNCTIONAL_CASE:READ+COMMENT']"
:preview-url="PreviewEditorImageUrl"
:is-active="isActive"
mode="textarea"
is-show-avatar
is-use-bottom
:upload-image="handleUploadImage"
@publish="publishHandler"
@cancel="cancelPublish"
/>
</div>
<div v-else class="pl-[16px]">
<a-button v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])" class="mr-3" type="primary" @click="linkBug">
{{ t('caseManagement.featureCase.linkDefect') }}
</a-button>
<a-button v-permission="['PROJECT_BUG:READ+ADD']" type="outline" @click="createBug"
>{{ t('caseManagement.featureCase.createDefect') }}
</a-button>
<div class="bug-list">
<div v-for="item of bugList" :key="item.id" class="bug-item">
<div class="mb-[4px] flex items-center justify-between">
<MsButton type="text" @click="goBug(item.id)">{{ item.num }}</MsButton>
<MsButton type="text" @click="disassociateBug(item.id)">
{{ t('ms.add.attachment.cancelAssociate') }}
</MsButton>
</div>
<a-tooltip :content="item.name">
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div>
<MsEmpty v-if="bugList.length === 0" />
</div>
</div>
</template>
@ -300,6 +268,7 @@
editorUploadFile,
getAssociatedFileListUrl,
getCaseDefaultFields,
getCaseDetail,
getCaseMinder,
getCaseModuleTree,
getCommentList,
@ -317,14 +286,15 @@
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { downloadByteFile, getGenerateId } from '@/utils';
import { downloadByteFile, getGenerateId, mapTree, traverseTree } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { AssociatedList, OptionsFieldId } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode, TableQueryParams } from '@/models/common';
import { TableQueryParams } from '@/models/common';
import { BugManagementRouteEnum } from '@/enums/routeEnum';
import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils';
import { Api } from '@form-create/arco-design';
const AddDefectDrawer = defineAsyncComponent(
() => import('@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue')
@ -336,6 +306,7 @@
const props = defineProps<{
moduleId: string;
moduleName: string;
modulesCount: Record<string, number>; //
}>();
const router = useRouter();
@ -344,14 +315,67 @@
const userStore = useUserStore();
const { t } = useI18n();
const caseTag = t('common.case');
const moduleTag = t('common.module');
const topTags = [moduleTag, caseTag];
const descTags = [t('ms.minders.stepDesc'), t('ms.minders.textDesc')];
const importJson = ref<MinderJson>({
root: {},
template: 'default',
treePath: [],
});
const caseTree = ref<MinderJsonNode[]>([]);
const loading = ref(false);
async function initCaseTree() {
try {
loading.value = true;
const res = await getCaseModuleTree({
projectId: appStore.currentProjectId,
moduleId: props.moduleId === 'all' ? '' : props.moduleId,
});
caseTree.value = mapTree<MinderJsonNode>(res, (e) => ({
...e,
data: {
id: e.id,
text: e.name,
resource: e.data?.id === 'fakeNode' ? [] : [moduleTag],
expandState: e.level === 1 ? 'expand' : 'collapse',
count: props.modulesCount[e.id],
},
children:
props.modulesCount[e.id] > 0 && !e.children?.length
? [
{
data: {
id: 'fakeNode',
text: 'fakeNode',
resource: ['fakeNode'],
},
},
]
: e.children,
}));
importJson.value.root = {
children: caseTree.value,
data: {
id: 'all',
text: t('ms.minders.allModule'),
resource: [moduleTag],
},
};
window.minder.importJson(importJson.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
async function initMinder() {
try {
loading.value = true;
const res = await getCaseMinder({
projectId: appStore.currentProjectId,
moduleId: props.moduleId === 'all' ? '' : props.moduleId,
@ -366,55 +390,19 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watchEffect(() => {
if (props.moduleId) {
if (props.moduleId === 'all') {
initCaseTree();
} else {
initMinder();
}
});
const caseTag = t('common.case');
const moduleTag = t('common.module');
const topTags = [moduleTag, caseTag];
const descTags = [t('ms.minders.stepDesc'), t('ms.minders.textDesc')];
const tags = [...topTags, t('ms.minders.precondition'), ...descTags, t('ms.minders.stepExpect'), t('common.remark')];
const visible = ref<boolean>(false);
const activeCase = ref<any>({});
const extractContentTabList = computed(() => {
const fullTabList = [
{
label: t('common.baseInfo'),
value: 'baseInfo',
},
{
label: t('caseManagement.featureCase.attachment'),
value: 'attachment',
},
{
value: 'comments',
label: t('caseManagement.featureCase.comments'),
},
{
value: 'bug',
label: t('caseManagement.featureCase.bug'),
},
];
if (activeCase.value.id) {
return fullTabList;
}
return fullTabList.filter((item) => item.value === 'baseInfo');
});
const activeExtraKey = ref<'baseInfo' | 'attachment' | 'comments' | 'bug'>('baseInfo');
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes(caseTag)) {
visible.value = true;
activeCase.value = data;
}
}
async function handleMinderSave(data: any) {
try {
await saveCaseMinder({
@ -435,6 +423,10 @@
* @param node 选中节点
*/
function replaceableTags(node: MinderJsonNode) {
if (Object.keys(node.data || {}).length === 0 || node.data?.id === 'root') {
//
return [];
}
if (node.data?.resource?.some((e) => topTags.includes(e))) {
//
return !node.children || node.children.length === 0
@ -444,7 +436,7 @@
if (node.data?.resource?.some((e) => descTags.includes(e))) {
//
if (
node.data?.resource?.includes(t('ms.minders.stepDesc')) &&
node.data.resource.includes(t('ms.minders.stepDesc')) &&
(node.parent?.children?.filter((e) => e.data?.resource?.includes(t('ms.minders.stepDesc'))) || []).length > 1
) {
//
@ -453,7 +445,7 @@
return descTags.filter((tag) => !node.data?.resource?.includes(tag));
}
if (
(!node.data?.resource || node.data?.resource?.length === 0) &&
(!node.data?.resource || node.data.resource.length === 0) &&
(!node.parent?.data?.resource ||
node.parent?.data?.resource.length === 0 ||
node.parent?.data?.resource?.some((e) => topTags.includes(e)))
@ -469,6 +461,11 @@
return [];
}
/**
* 执行插入节点
* @param command 插入命令
* @param node 目标节点
*/
function execInert(command: string, node?: MinderJsonNodeData) {
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command, node);
@ -716,7 +713,7 @@
const rowLength = ref<number>(0);
const formRules = ref<FormItem[]>([]);
const formItem = ref<FormRuleItem[]>([]);
const fApi = ref<any>(null);
const fApi = ref<Api>();
//
async function initDefaultFields() {
formRules.value = [];
@ -734,7 +731,10 @@
initValue = item.type === 'MEMBER' ? userStore.id : [userStore.id];
}
}
if (item.internal && item.type === 'SELECT') {
// TODO:
return false;
}
return {
type: item.type,
name: item.fieldId,
@ -748,7 +748,7 @@
},
};
});
formRules.value = result;
formRules.value = result.filter((e: any) => e);
baseInfoLoading.value = false;
} catch (error) {
// eslint-disable-next-line no-console
@ -756,27 +756,116 @@
}
}
const caseTree = ref<ModuleTreeNode[]>([]);
const extraVisible = ref<boolean>(false);
const activeCase = ref<Record<string, any>>({});
const extractContentTabList = computed(() => {
const fullTabList = [
{
label: t('common.baseInfo'),
value: 'baseInfo',
},
{
label: t('caseManagement.featureCase.attachment'),
value: 'attachment',
},
{
value: 'comments',
label: t('caseManagement.featureCase.comments'),
},
{
value: 'bug',
label: t('caseManagement.featureCase.bug'),
},
];
if (activeCase.value.id) {
return fullTabList;
}
return fullTabList.filter((item) => item.value === 'baseInfo');
});
const activeExtraKey = ref<'baseInfo' | 'attachment' | 'comments' | 'bug'>('baseInfo');
async function initSelectTree() {
try {
caseTree.value = await getCaseModuleTree({ projectId: appStore.currentProjectId });
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
async function handleNodeClick(node: MinderJsonNode) {
const { data } = node;
if (data?.resource && data.resource.includes(caseTag)) {
extraVisible.value = true;
try {
baseInfoLoading.value = true;
const res = await getCaseDetail(data.id);
activeCase.value = res;
baseInfoForm.value.name = res.name;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
baseInfoLoading.value = false;
}
} else if (data?.resource?.includes(moduleTag) && data.count > 0 && data.isLoaded !== true) {
try {
loading.value = true;
const res = await getCaseMinder({
projectId: appStore.currentProjectId,
moduleId: data.id,
});
const fakeNode = node.children?.find((e) => e.data?.id === undefined); //
window.minder.removeNode(fakeNode);
res.forEach((e) => {
//
const child = window.minder.createNode(e.data, node);
child.render();
e.children?.forEach((item) => {
// //
const grandChild = window.minder.createNode(item.data, child);
grandChild.render();
item.children?.forEach((subItem) => {
//
const greatGrandChild = window.minder.createNode(subItem.data, grandChild);
greatGrandChild.render();
});
child.renderTree();
});
child.expand();
child.renderTree();
});
node.expand();
node.renderTree();
window.minder.layout();
if (node.data) {
node.data.isLoaded = true;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
} else {
extraVisible.value = false;
activeCase.value = {};
}
}
onBeforeMount(() => {
initDefaultFields();
initSelectTree();
});
function handleSave() {
if (activeExtraKey.value === 'baseInfo') {
baseInfoFormRef.value?.validate((errors) => {
if (!errors) {
Message.success(t('common.saveSuccess'));
fApi.value?.validate((valid) => {
if (valid) {
const data = {
...baseInfoForm.value,
customFields: formItem.value.map((item: any) => {
return {
fieldId: item.field,
value: Array.isArray(item.value) ? JSON.stringify(item.value) : item.value,
};
}),
};
console.log(data);
}
});
}
});
}
@ -1130,6 +1219,12 @@
:deep(.commentWrapper) {
right: 0;
}
.baseInfo-form {
.ms-scroll-bar();
overflow-y: auto;
height: calc(100% - 64px);
}
.bug-list {
.ms-scroll-bar();

View File

@ -1,5 +1,5 @@
<template>
<FeatureCaseMinder :module-id="props.moduleId" :module-name="props.moduleName" />
<FeatureCaseMinder :module-id="props.moduleId" :module-name="props.moduleName" :modules-count="props.modulesCount" />
</template>
<script setup lang="ts">
@ -9,6 +9,7 @@
minderType: 'FeatureCase';
moduleId: string;
moduleName: string;
modulesCount: Record<string, number>; //
}>();
</script>

View File

@ -1,4 +1,5 @@
export default {
'ms.minders.allModule': '全部模块',
'ms.minders.precondition': '前置条件',
'ms.minders.stepDesc': '步骤描述',
'ms.minders.stepExpect': '预期结果',

View File

@ -358,8 +358,8 @@
const id = new Date().getTime().toString();
propsRes.value.data.push({
id,
...cloneDeep(props.defaultParamDataItem), //
enable: true, //
...cloneDeep(props.defaultParamDataItem), //
} as any);
emitChange('addTableLine', isInit);
}

View File

@ -109,5 +109,6 @@ export default {
delete: 'Delete',
enterNode: 'Enter the current node',
},
loading: 'Mind map loading...',
},
};

View File

@ -103,5 +103,6 @@ export default {
delete: '删除',
enterNode: '进入当前节点',
},
loading: '脑图加载中...',
},
};

View File

@ -1,5 +1,5 @@
<template>
<div class="ms-minder-editor-container">
<a-spin :loading="loading" :tip="t('minder.loading')" class="ms-minder-editor-container">
<div class="flex-1">
<minderHeader
:sequence-enable="props.sequenceEnable"
@ -49,15 +49,17 @@
@enter-node="handleEnterNode"
/>
</div>
<div v-if="props.extractContentTabList?.length" class="ms-minder-editor-extra">
<div class="pl-[16px] pt-[16px]">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
<template v-if="props.extractContentTabList?.length">
<div class="ms-minder-editor-extra" :class="[extraVisible ? 'ms-minder-editor-extra--visible' : '']">
<div class="pl-[16px] pt-[16px]">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
</div>
<div class="ms-minder-editor-extra-content">
<slot name="extractTabContent"></slot>
</div>
</div>
<div class="ms-minder-editor-extra-content">
<slot name="extractTabContent"></slot>
</div>
</div>
</div>
</template>
</a-spin>
</template>
<script lang="ts" name="minderEditor" setup>
@ -65,12 +67,15 @@
import minderHeader from './main/header.vue';
import mainEditor from './main/mainEditor.vue';
import { useI18n } from '@/hooks/useI18n';
import {
delProps,
editMenuProps,
headerProps,
insertProps,
mainEditorProps,
MinderJsonNode,
moleProps,
priorityProps,
tagProps,
@ -97,6 +102,18 @@
...viewMenuProps,
});
const { t } = useI18n();
const loading = defineModel<boolean>('loading', {
default: false,
});
const activeExtraKey = defineModel<string>('activeExtraKey', {
default: '',
});
const extraVisible = defineModel<boolean>('extraVisible', {
default: false,
});
onMounted(async () => {
window.minderProps = props;
});
@ -113,19 +130,15 @@
emit('enterNode', data);
}
const activeExtraKey = defineModel<string>('activeExtraKey', {
default: '',
});
onMounted(() => {
nextTick(() => {
if (window.minder.on) {
window.minder.on('mousedown', (e: any) => {
if (e.originEvent.button === 0) {
//
const selectedNode = window.minder.getSelectedNode();
const selectedNode: MinderJsonNode = window.minder.getSelectedNode();
if (Object.keys(window.minder).length > 0 && selectedNode) {
emit('nodeClick', selectedNode.data);
emit('nodeClick', selectedNode);
}
}
});
@ -136,13 +149,13 @@
<style lang="less" scoped>
.ms-minder-editor-container {
@apply relative flex h-full;
@apply relative flex h-full w-full;
.ms-minder-editor-extra {
@apply flex flex-col border-l;
@apply flex flex-col overflow-hidden border-l;
width: 35%;
min-width: 360px;
width: 0;
border-color: var(--color-text-n8);
transition: all 300ms ease-in-out;
.ms-minder-editor-extra-content {
@apply relative flex-1 overflow-y-auto;
.ms-scroll-bar();
@ -150,5 +163,9 @@
margin-top: 16px;
}
}
.ms-minder-editor-extra--visible {
width: 35%;
transition: all 300ms ease-in-out;
}
}
</style>

View File

@ -13,13 +13,16 @@ export interface MinderJsonNodeData {
id: string;
text: string;
resource?: string[];
expandState?: string;
expandState?: 'collapse' | 'expand';
priority?: number;
// 前端渲染字段
[key: string]: any;
}
export interface MinderJsonNode {
parent?: MinderJsonNode;
data?: MinderJsonNodeData;
children?: MinderJsonNode[];
[key: string]: any; // minder 内置字段
}
export interface MinderJson {

View File

@ -2,6 +2,7 @@ import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import router from '@/router';
import { WHITE_LIST } from '@/router/constants';
import { useAppStore, useUserStore } from '@/store';
export default function useUser() {
@ -33,8 +34,14 @@ export default function useUser() {
return window.location.hash.indexOf('login') > -1;
};
const isWhiteListPage = () => {
const currentRoute = router.currentRoute.value;
return WHITE_LIST.some((e) => e.path.includes(currentRoute.path));
};
return {
logout,
isLoginPage,
isWhiteListPage,
};
}

View File

@ -257,6 +257,7 @@ export interface CsvVariable {
};
// 以下为前端字段
settingVisible: boolean;
copyId?: string; // 复制场景时,源场景的 csv 参数的 id
}
export interface CommonVariable {
id: string | number;
@ -412,6 +413,7 @@ export interface Scenario {
stepFileParam: Record<string, ScenarioStepFileParams>;
fileParam: ScenarioFileParams;
follow?: boolean;
copyFromScenarioId?: string | number;
uploadFileIds: string[];
linkFileIds: string[];
// 前端渲染字段

View File

@ -230,6 +230,12 @@
:disabled-param-value="props.disabled"
:scroll="{ x: '100%' }"
:columns="scriptColumns"
:default-param-item="{
key: '',
value: '',
description: '',
required: false,
}"
:selectable="false"
@change="() => emit('change')"
/>

View File

@ -13,7 +13,6 @@ import {
ResponseDefinition,
} from '@/models/apiTest/common';
import type { MockParams } from '@/models/apiTest/mock';
import type { CsvVariable } from '@/models/apiTest/scenario';
import {
FullResponseAssertionType,
RequestAssertionCondition,
@ -414,37 +413,3 @@ export const matchRuleOptions = [
];
// mock 参数为文件类型的匹配规则选项
export const mockFileMatchRules = ['EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY'];
// 场景-常规参数默认值
export const defaultNormalParamItem = {
key: '',
paramType: 'CONSTANT',
value: '',
description: '',
tags: [],
enable: true,
};
// 场景-csv参数默认值
export const defaultCsvParamItem: CsvVariable = {
id: '',
scenarioId: '',
name: '',
scope: 'SCENARIO',
enable: false,
encoding: 'UTF-8',
random: false,
variableNames: '',
ignoreFirstLine: false,
delimiter: ',',
allowQuotedData: false,
recycleOnEof: false,
stopThreadOnEof: false,
settingVisible: false,
file: {
fileId: '',
fileName: '',
local: false,
fileAlias: '',
delete: false,
},
};

View File

@ -495,9 +495,9 @@
<div v-else class="text-[var(--color-text-1)]">{{ '-' }}</div>
</template>
<!-- 单独启用/禁用列 -->
<template #enable="{ record, rowIndex }">
<template v-if="!props.selectable" #enable="{ record, rowIndex }">
<a-switch
v-model="record.enable"
v-model:model-value="record.enable"
:disabled="props.disabledExceptParam"
size="small"
type="line"
@ -513,7 +513,7 @@
<div class="flex w-full flex-row items-center" :class="{ 'justify-end': columnConfig.align === 'right' }">
<a-switch
v-if="columnConfig.hasDisable"
v-model="record.enable"
v-model:model-value="record.enable"
:disabled="props.disabledExceptParam"
size="small"
type="line"
@ -664,7 +664,7 @@
const props = withDefaults(
defineProps<{
params?: Record<string, any>[];
defaultParamItem?: Record<string, any>; //
defaultParamItem: Record<string, any>; //
columns: ParamTableColumn[];
scroll?: {
x?: number | string;
@ -703,20 +703,6 @@
showSetting: false,
tableKey: undefined,
isSimpleSetting: true,
defaultParamItem: () => ({
required: false,
key: '',
paramType: RequestParamsType.STRING,
value: '',
minLength: undefined,
maxLength: undefined,
contentType: RequestContentTypeEnum.TEXT,
tag: [],
description: '',
encode: false,
disable: false,
mustContain: false,
}),
}
);
const emit = defineEmits<{
@ -942,7 +928,7 @@
if (
(!props.disabledExceptParam || !props.disabledParamValue) &&
hasNoIdItem &&
!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault &&
!filterKeyValParams(arr, props.defaultParamItem, !props.selectable).lastDataIsDefault &&
!props.isTreeTable
) {
addTableLine(arr.length - 1, false, true);

View File

@ -179,11 +179,13 @@ export function filterKeyValParams<T>(
}
// id、enable、valid属性不参与比较
delete lastData.id;
delete lastData.enable;
delete lastData.valid;
delete defaultParam.valid;
delete defaultParam.id;
delete defaultParam.enable;
delete defaultParam.valid;
if (!filterEnable) {
delete lastData.enable;
delete defaultParam.enable;
}
const lastDataIsDefault = isEqual(lastData, defaultParam) || lastData.key === '';
let validParams: (T & Record<string, any>)[];
if (lastDataIsDefault) {

View File

@ -12,7 +12,7 @@
:file-save-as-source-id="props.scenarioId"
:file-save-as-api="transferFile"
:file-module-options-api="getTransferOptions"
@change="handleCsvVariablesChange"
@change="(data) => handleCsvVariablesChange(data as CsvVariable[])"
>
<template #operationPre="{ record }">
<a-trigger
@ -123,7 +123,7 @@
import { CsvVariable } from '@/models/apiTest/scenario';
import { defaultCsvParamItem } from '@/views/api-test/components/config';
import { defaultCsvParamItem } from '../config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{
@ -209,8 +209,8 @@
},
];
function handleCsvVariablesChange(resultArr: any[], isInit?: boolean) {
csvVariables.value = resultArr.map((e) => ({ ...e, enable: e.name && e.fileId }));
function handleCsvVariablesChange(resultArr: CsvVariable[], isInit?: boolean) {
csvVariables.value = resultArr.map((e) => ({ ...e, enable: e.name && e.file.fileId ? e.enable : false }));
if (!isInit) {
emit('change');
}
@ -309,6 +309,7 @@
Message.warning(t('apiScenario.csvFileNotNull'));
return false;
}
return true;
}
return true;
}

View File

@ -31,7 +31,7 @@
import { CsvVariable } from '@/models/apiTest/scenario';
import { defaultCsvParamItem } from '@/views/api-test/components/config';
import { defaultCsvParamItem } from '../config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{

View File

@ -1,4 +1,4 @@
import { Scenario, ScenarioStepConfig } from '@/models/apiTest/scenario';
import { type CsvVariable, Scenario, ScenarioStepConfig } from '@/models/apiTest/scenario';
import {
ApiScenarioStatus,
RequestAssertionCondition,
@ -180,3 +180,38 @@ export const conditionOptions = [
label: 'apiScenario.notNull',
},
];
// 场景-常规参数默认值
export const defaultNormalParamItem = {
key: '',
paramType: 'CONSTANT',
value: '',
description: '',
tags: [],
enable: true,
};
// 场景-csv参数默认值
export const defaultCsvParamItem: CsvVariable = {
id: '',
scenarioId: '',
name: '',
scope: 'SCENARIO',
enable: false,
encoding: 'UTF-8',
random: false,
variableNames: '',
ignoreFirstLine: false,
delimiter: ',',
allowQuotedData: false,
recycleOnEof: false,
stopThreadOnEof: false,
settingVisible: false,
file: {
fileId: '',
fileName: '',
local: false,
fileAlias: '',
delete: false,
},
};

View File

@ -54,7 +54,7 @@
import { CommonVariable, CsvVariable } from '@/models/apiTest/scenario';
import { defaultNormalParamItem } from '@/views/api-test/components/config';
import { defaultNormalParamItem } from './config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{

View File

@ -890,11 +890,14 @@
function handleQuoteCsvConfirm(keys: string[]) {
if (activeStep.value) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.uniqueId, 'uniqueId');
if (replaceCsvId.value && realStep) {
if (!!replaceCsvId.value && realStep !== null) {
const index = realStep.csvIds.findIndex((item: string) => item === replaceCsvId.value);
realStep.csvIds?.splice(index, 1, keys[0]);
} else if (realStep) {
realStep.csvIds?.push(...keys);
if (!realStep.csvIds) {
realStep.csvIds = [];
}
realStep.csvIds.splice(index, 1, keys[0]);
} else if (realStep !== null) {
realStep.csvIds = [...(realStep.csvIds || []), ...keys];
}
}
}

View File

@ -146,8 +146,7 @@
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { defaultCsvParamItem, defaultNormalParamItem } from '../components/config';
import { defaultScenario } from './components/config';
import { defaultCsvParamItem, defaultNormalParamItem, defaultScenario } from './components/config';
import updateStepStatus, { getScenarioFileParams } from './components/utils';
import {
filterAssertions,
@ -382,6 +381,11 @@
if (defaultScenarioInfo) {
const isCopy = action === 'copy';
let copySteps: ScenarioStepItem[] = [];
const copyCsvVariables = defaultScenarioInfo.scenarioConfig.variable.csvVariables.map((e) => ({
...e,
copyId: e.id,
id: getGenerateId(),
}));
if (isCopy) {
// copyFromStepId
copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
@ -406,6 +410,9 @@
//
node.id = getGenerateId(); // ID
}
if (node.csvIds && node.csvIds.length > 0) {
node.csvIds = node.csvIds.map((e: string) => copyCsvVariables.find((c) => c.copyId === e)?.id || '');
}
node.uniqueId = node.id;
return node;
});
@ -439,9 +446,17 @@
...defaultScenarioInfo,
steps: copySteps,
id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
copyFromScenarioId: isCopy ? defaultScenarioInfo.id : '',
label: isCopy ? copyName : defaultScenarioInfo.name,
name: isCopy ? copyName : defaultScenarioInfo.name,
isNew: isCopy,
scenarioConfig: {
...defaultScenarioInfo.scenarioConfig,
variable: {
commonVariables: defaultScenarioInfo.scenarioConfig.variable.commonVariables,
csvVariables: copyCsvVariables,
},
},
stepResponses: {},
errorMessageInfo: {},
});
@ -531,7 +546,8 @@
).validParams,
csvVariables: filterKeyValParams(
activeScenarioTab.value.scenarioConfig.variable.csvVariables,
defaultCsvParamItem
defaultCsvParamItem,
true
).validParams,
},
},
@ -587,6 +603,17 @@
preProcessorConfig: filterConditionsSqlValidParams(
activeScenarioTab.value.scenarioConfig.preProcessorConfig
),
variable: {
commonVariables: filterKeyValParams(
activeScenarioTab.value.scenarioConfig.variable.commonVariables,
defaultNormalParamItem
).validParams,
csvVariables: filterKeyValParams(
activeScenarioTab.value.scenarioConfig.variable.csvVariables,
defaultCsvParamItem,
true
).validParams,
},
},
environmentId: appStore.getCurrentEnvId || '',
steps: mapTree(activeScenarioTab.value.steps, (node) => {
@ -621,7 +648,7 @@
(e) => e.id === (typeof record === 'string' ? record : record.id)
);
if (isLoadedTabIndex > -1 && action !== 'copy') {
// tabtab
// tabtab
activeScenarioTab.value = scenarioTabs.value[isLoadedTabIndex];
// tabid,id
if (action === 'execute') {

View File

@ -30,14 +30,14 @@
</a-popover>
</template>
<template #right>
<a-radio-group v-model:model-value="showType" type="button" size="small" class="list-show-type">
<!-- <a-radio-group v-model:model-value="showType" type="button" size="small" class="list-show-type">
<a-radio value="list" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_view-list_outlined" />
</a-radio>
<!-- <a-radio value="xMind" class="show-type-icon !m-[2px]">
<a-radio value="xMind" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio>-->
</a-radio-group>
</a-radio>
</a-radio-group> -->
</template>
</MsAdvanceFilter>
<ms-base-table
@ -54,9 +54,9 @@
@cell-click="handleCellClick"
>
<template #num="{ record }">
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">{{
record.num
}}</span>
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">
{{ record.num }}
</span>
</template>
<template #name="{ record }">
<div class="one-line-text">{{ characterLimit(record.name) }}</div>
@ -219,7 +219,12 @@
</div>
<div class="mt-[16px] h-[calc(100%-32px)] border-t border-[var(--color-text-n8)]">
<!-- 脑图开始 -->
<MsMinder minder-type="FeatureCase" :module-id="props.activeFolder" :module-name="props.moduleName" />
<MsMinder
minder-type="FeatureCase"
:module-id="props.activeFolder"
:modules-count="props.modulesCount"
:module-name="props.moduleName"
/>
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>