refactor(用例脑图): 用例脑图代码结构拆分&mock 表格显示子模块设置
This commit is contained in:
parent
6619efe9a3
commit
c89e7902f9
|
@ -60,7 +60,6 @@
|
|||
:item-border="false"
|
||||
class="w-full rounded-[var(--border-radius-small)]"
|
||||
:no-more-data="noMoreData"
|
||||
raggable
|
||||
:virtual-list-props="{
|
||||
height: 'calc(100vh - 136px)',
|
||||
}"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,335 @@
|
|||
<template>
|
||||
<a-spin :loading="attachmentLoading" class="block h-full pl-[16px]">
|
||||
<MsAddAttachment
|
||||
v-model:file-list="fileList"
|
||||
multiple
|
||||
only-button
|
||||
@change="(files, file) => handleFileChange(file ? [file] : [])"
|
||||
@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"
|
||||
:handle-delete="deleteFileHandler"
|
||||
show-delete
|
||||
button-in-title
|
||||
>
|
||||
<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>
|
||||
<template #titleAction="{ 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>
|
||||
</MsFileList>
|
||||
</a-spin>
|
||||
<LinkFileDrawer
|
||||
v-model:visible="showLinkFileDrawer"
|
||||
:get-tree-request="getModules"
|
||||
:get-count-request="getModulesCount"
|
||||
:get-list-request="getAssociatedFileListUrl"
|
||||
:get-list-fun-params="getListFunParams"
|
||||
@save="saveSelectAssociatedFile"
|
||||
/>
|
||||
<a-image-preview v-model:visible="previewVisible" :src="imageUrl" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
||||
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
|
||||
import SaveAsFilePopover from '@/components/business/ms-add-attachment/saveAsFilePopover.vue';
|
||||
import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
|
||||
|
||||
import {
|
||||
deleteFileOrCancelAssociation,
|
||||
downloadFileRequest,
|
||||
getAssociatedFileListUrl,
|
||||
getTransferFileTree,
|
||||
previewFile,
|
||||
transferFileRequest,
|
||||
updateFile,
|
||||
uploadOrAssociationFile,
|
||||
} from '@/api/modules/case-management/featureCase';
|
||||
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { downloadByteFile } from '@/utils';
|
||||
|
||||
import { AssociatedList } from '@/models/caseManagement/featureCase';
|
||||
import { TableQueryParams } from '@/models/common';
|
||||
|
||||
import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
activeCase: Record<string, any>;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'uploadSuccess'): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const fileList = defineModel<MsFileItem[]>({
|
||||
required: true,
|
||||
});
|
||||
|
||||
const getListFunParams = ref<TableQueryParams>({
|
||||
combine: {
|
||||
hiddenIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
// 监视文件列表处理关联和本地文件
|
||||
watch(
|
||||
() => fileList.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getListFunParams.value.combine.hiddenIds = fileList.value.filter((item) => !item.local).map((item) => item.uid);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const showLinkFileDrawer = ref(false);
|
||||
const attachmentLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 处理文件更改
|
||||
* @param _fileList 文件列表
|
||||
* @param isAssociated 是否是关联文件
|
||||
*/
|
||||
async function handleFileChange(_fileList: MsFileItem[], isAssociated = false) {
|
||||
try {
|
||||
attachmentLoading.value = true;
|
||||
const params = {
|
||||
request: {
|
||||
caseId: props.activeCase.id,
|
||||
projectId: appStore.currentProjectId,
|
||||
fileIds: isAssociated ? _fileList.map((item) => item.uid) : [],
|
||||
enable: true,
|
||||
},
|
||||
file: isAssociated ? _fileList.map((item) => item.file) : _fileList[0].file,
|
||||
};
|
||||
await uploadOrAssociationFile(params);
|
||||
if (isAssociated) {
|
||||
fileList.value.unshift(..._fileList);
|
||||
} else {
|
||||
fileList.value = fileList.value.map((item) => {
|
||||
if (item.status === 'init') {
|
||||
return { ...item, status: 'done', local: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
Message.success(t('ms.upload.uploadSuccess'));
|
||||
emit('uploadSuccess');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
fileList.value = fileList.value.map((item) => ({ ...item, status: 'error' }));
|
||||
} finally {
|
||||
attachmentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关联文件
|
||||
* @param fileData 文件信息集合
|
||||
*/
|
||||
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
|
||||
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
|
||||
handleFileChange(fileResultList, true);
|
||||
}
|
||||
|
||||
const imageUrl = ref('');
|
||||
const previewVisible = ref<boolean>(false);
|
||||
|
||||
// 预览图片
|
||||
async function handlePreview(item: MsFileItem) {
|
||||
try {
|
||||
imageUrl.value = '';
|
||||
previewVisible.value = true;
|
||||
if (!item.local) {
|
||||
const res = await previewFile({
|
||||
projectId: appStore.currentProjectId,
|
||||
caseId: props.activeCase.id,
|
||||
fileId: item.uid,
|
||||
local: item.local,
|
||||
});
|
||||
const blob = new Blob([res], { type: 'image/jpeg' });
|
||||
imageUrl.value = URL.createObjectURL(blob);
|
||||
} else {
|
||||
imageUrl.value = item.url || '';
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const transferVisible = ref<boolean>(false);
|
||||
|
||||
const activeTransferFileParams = ref<MsFileItem>();
|
||||
|
||||
// 转存
|
||||
function transferFile(item: MsFileItem) {
|
||||
activeTransferFileParams.value = { ...item };
|
||||
transferVisible.value = true;
|
||||
}
|
||||
|
||||
// 删除本地文件
|
||||
async function deleteFileHandler(item: MsFileItem) {
|
||||
if (!item.local) {
|
||||
try {
|
||||
const params = {
|
||||
id: item.uid,
|
||||
local: item.local,
|
||||
caseId: props.activeCase.id,
|
||||
projectId: appStore.currentProjectId,
|
||||
};
|
||||
await deleteFileOrCancelAssociation(params);
|
||||
Message.success(
|
||||
item.local ? t('caseManagement.featureCase.deleteSuccess') : t('caseManagement.featureCase.cancelLinkSuccess')
|
||||
);
|
||||
fileList.value = fileList.value.filter((e) => e.uid !== item.uid);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
} else {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('caseManagement.featureCase.deleteFile', { name: item?.name }),
|
||||
content: t('caseManagement.featureCase.deleteFileTip'),
|
||||
okText: t('common.confirmDelete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
const params = {
|
||||
id: item.uid,
|
||||
local: item.local,
|
||||
caseId: props.activeCase.id,
|
||||
projectId: appStore.currentProjectId,
|
||||
};
|
||||
await deleteFileOrCancelAssociation(params);
|
||||
Message.success(
|
||||
item.local
|
||||
? t('caseManagement.featureCase.deleteSuccess')
|
||||
: t('caseManagement.featureCase.cancelLinkSuccess')
|
||||
);
|
||||
fileList.value = fileList.value.filter((e) => e.uid !== item.uid);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 下载
|
||||
async function downloadFile(item: MsFileItem) {
|
||||
try {
|
||||
const res = await downloadFileRequest({
|
||||
projectId: appStore.currentProjectId,
|
||||
caseId: props.activeCase.id,
|
||||
fileId: item.uid,
|
||||
local: item.local,
|
||||
});
|
||||
downloadByteFile(res, `${item.name}`);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新文件
|
||||
async function handleUpdateFile(item: MsFileItem) {
|
||||
try {
|
||||
await updateFile(appStore.currentProjectId, item.associationId);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,186 @@
|
|||
<template>
|
||||
<div class="h-full pl-[16px]">
|
||||
<div class="baseInfo-form">
|
||||
<a-skeleton v-if="baseInfoLoading || props.loading" :loading="baseInfoLoading || props.loading" :animation="true">
|
||||
<a-space direction="vertical" class="w-full" size="large">
|
||||
<a-skeleton-line :rows="10" :line-height="30" :line-spacing="30" />
|
||||
</a-space>
|
||||
</a-skeleton>
|
||||
<a-form v-else ref="baseInfoFormRef" :model="baseInfoForm" layout="vertical">
|
||||
<a-form-item
|
||||
field="name"
|
||||
:label="t('ms.minders.caseName')"
|
||||
:rules="[{ required: true, message: t('ms.minders.caseNameNotNull') }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-input v-model:model-value="baseInfoForm.name" :placeholder="t('common.pleaseInput')" allow-clear></a-input>
|
||||
</a-form-item>
|
||||
<MsFormCreate
|
||||
v-if="formRules.length"
|
||||
ref="formCreateRef"
|
||||
v-model:api="fApi"
|
||||
v-model:form-item="formItem"
|
||||
:form-rule="formRules"
|
||||
/>
|
||||
<a-form-item field="tags" :label="t('common.tag')">
|
||||
<MsTagsInput v-model:model-value="baseInfoForm.tags" :max-tag-count="6" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="flex items-center gap-[12px] bg-white py-[16px]">
|
||||
<a-button
|
||||
v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"
|
||||
type="primary"
|
||||
:loading="saveLoading"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
<a-button type="secondary" :disabled="saveLoading">{{ t('common.cancel') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsFormCreate from '@/components/pure/ms-form-create/ms-form-create.vue';
|
||||
import { FormItem, FormRuleItem } from '@/components/pure/ms-form-create/types';
|
||||
import { MinderJsonNode } from '@/components/pure/ms-minder-editor/props';
|
||||
|
||||
import { getCaseDefaultFields, updateCaseRequest } from '@/api/modules/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
|
||||
import { OptionsFieldId } from '@/models/caseManagement/featureCase';
|
||||
|
||||
import { Api } from '@form-create/arco-design';
|
||||
|
||||
const props = defineProps<{
|
||||
activeCase: Record<string, any>;
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const baseInfoFormRef = ref<FormInstance>();
|
||||
const baseInfoForm = ref({
|
||||
name: '',
|
||||
tags: [],
|
||||
templateId: '',
|
||||
moduleId: 'root',
|
||||
});
|
||||
const baseInfoLoading = ref(false);
|
||||
|
||||
const formRules = ref<FormItem[]>([]);
|
||||
const formItem = ref<FormRuleItem[]>([]);
|
||||
const fApi = ref<Api>();
|
||||
// 初始化模板默认字段
|
||||
async function initDefaultFields() {
|
||||
formRules.value = [];
|
||||
try {
|
||||
baseInfoLoading.value = true;
|
||||
const res = await getCaseDefaultFields(appStore.currentProjectId);
|
||||
const { customFields, id } = res;
|
||||
baseInfoForm.value.templateId = id;
|
||||
const result = customFields.map((item: any) => {
|
||||
const memberType = ['MEMBER', 'MULTIPLE_MEMBER'];
|
||||
let initValue = item.defaultValue;
|
||||
const optionsValue: OptionsFieldId[] = item.options;
|
||||
if (memberType.includes(item.type)) {
|
||||
if (item.defaultValue === 'CREATE_USER' || item.defaultValue.includes('CREATE_USER')) {
|
||||
initValue = item.type === 'MEMBER' ? userStore.id : [userStore.id];
|
||||
}
|
||||
}
|
||||
if (item.internal && item.type === 'SELECT') {
|
||||
// TODO:过滤用例等级字段,等级字段后续可自定义,需要调整
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
type: item.type,
|
||||
name: item.fieldId,
|
||||
label: item.fieldName,
|
||||
value: initValue,
|
||||
required: item.required,
|
||||
options: optionsValue || [],
|
||||
props: {
|
||||
modelValue: initValue,
|
||||
options: optionsValue || [],
|
||||
},
|
||||
};
|
||||
});
|
||||
formRules.value = result.filter((e: any) => e);
|
||||
baseInfoLoading.value = false;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initDefaultFields();
|
||||
});
|
||||
|
||||
const saveLoading = ref(false);
|
||||
function handleSave() {
|
||||
baseInfoFormRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
fApi.value?.validate(async (valid) => {
|
||||
if (valid === true) {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
const data = {
|
||||
...baseInfoForm.value,
|
||||
id: props.activeCase.id,
|
||||
projectId: appStore.currentProjectId,
|
||||
caseEditType: props.activeCase.caseEditType,
|
||||
customFields: formItem.value.map((item: any) => {
|
||||
return {
|
||||
fieldId: item.field,
|
||||
value: Array.isArray(item.value) ? JSON.stringify(item.value) : item.value,
|
||||
};
|
||||
}),
|
||||
};
|
||||
await updateCaseRequest({
|
||||
request: data,
|
||||
fileList: [],
|
||||
});
|
||||
const selectedNode: MinderJsonNode = window.minder.getSelectedNode();
|
||||
if (selectedNode.data) {
|
||||
selectedNode.data.text = baseInfoForm.value.name;
|
||||
}
|
||||
Message.success(t('common.saveSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.activeCase.id,
|
||||
() => {
|
||||
baseInfoForm.value.name = props.activeCase.name;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.baseInfo-form {
|
||||
.ms-scroll-bar();
|
||||
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,203 @@
|
|||
<template>
|
||||
<a-spin :loading="bugListLoading" class="block h-full 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>
|
||||
<MsList
|
||||
v-model:data="bugList"
|
||||
mode="remote"
|
||||
item-key-field="id"
|
||||
:item-border="false"
|
||||
class="my-[16px] h-[calc(100%-64px)] w-full rounded-[var(--border-radius-small)]"
|
||||
:no-more-data="noMoreData"
|
||||
:virtual-list-props="{
|
||||
height: '100%',
|
||||
}"
|
||||
@reach-bottom="handleReachBottom"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div 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" position="tl">
|
||||
<div class="one-line-text">{{ item.name }}</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</MsList>
|
||||
</a-spin>
|
||||
<AddDefectDrawer
|
||||
v-if="activeCase.id"
|
||||
v-model:visible="showCreateBugDrawer"
|
||||
:case-id="activeCase.id"
|
||||
:extra-params="{ caseId: activeCase.id }"
|
||||
@success="loadBugList"
|
||||
/>
|
||||
<LinkDefectDrawer
|
||||
v-if="activeCase.id"
|
||||
v-model:visible="showLinkBugDrawer"
|
||||
:case-id="activeCase.id"
|
||||
:drawer-loading="drawerLoading"
|
||||
@save="saveHandler"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsList from '@/components/pure/ms-list/index.vue';
|
||||
|
||||
import {
|
||||
associatedDebug,
|
||||
cancelAssociatedDebug,
|
||||
getLinkedCaseBugList,
|
||||
} from '@/api/modules/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
|
||||
import { TableQueryParams } from '@/models/common';
|
||||
import { BugManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const AddDefectDrawer = defineAsyncComponent(
|
||||
() => import('@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue')
|
||||
);
|
||||
const LinkDefectDrawer = defineAsyncComponent(
|
||||
() => import('@/views/case-management/caseManagementFeature/components/tabContent/tabBug/linkDefectDrawer.vue')
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
activeCase: Record<string, any>;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const bugList = ref<any[]>([]);
|
||||
const noMoreData = ref(false);
|
||||
const pageNation = ref({
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
current: 1,
|
||||
});
|
||||
const bugListLoading = ref(false);
|
||||
|
||||
async function loadBugList() {
|
||||
try {
|
||||
bugListLoading.value = true;
|
||||
const res = await getLinkedCaseBugList({
|
||||
keyword: '',
|
||||
projectId: appStore.currentProjectId,
|
||||
caseId: props.activeCase.id,
|
||||
current: pageNation.value.current || 1,
|
||||
pageSize: pageNation.value.pageSize,
|
||||
});
|
||||
if (pageNation.value.current === 1) {
|
||||
bugList.value = res.list;
|
||||
} else {
|
||||
bugList.value = bugList.value.concat(res.list);
|
||||
}
|
||||
pageNation.value.total = res.total;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
bugListLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动翻页
|
||||
function handleReachBottom() {
|
||||
pageNation.value.current += 1;
|
||||
if (pageNation.value.current > Math.ceil(pageNation.value.total / pageNation.value.pageSize)) {
|
||||
return;
|
||||
}
|
||||
loadBugList();
|
||||
}
|
||||
|
||||
const cancelLoading = ref<boolean>(false);
|
||||
// 取消关联
|
||||
async function disassociateBug(id: string) {
|
||||
cancelLoading.value = true;
|
||||
try {
|
||||
await cancelAssociatedDebug(id);
|
||||
bugList.value = bugList.value.filter((item) => item.id !== id);
|
||||
Message.success(t('caseManagement.featureCase.cancelLinkSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
cancelLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBug(id: string) {
|
||||
router.push({
|
||||
name: BugManagementRouteEnum.BUG_MANAGEMENT_INDEX,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const showCreateBugDrawer = ref<boolean>(false);
|
||||
function createBug() {
|
||||
showCreateBugDrawer.value = true;
|
||||
}
|
||||
|
||||
const showLinkBugDrawer = ref<boolean>(false);
|
||||
|
||||
function linkBug() {
|
||||
showLinkBugDrawer.value = true;
|
||||
}
|
||||
|
||||
const drawerLoading = ref<boolean>(false);
|
||||
async function saveHandler(params: TableQueryParams) {
|
||||
try {
|
||||
drawerLoading.value = true;
|
||||
await associatedDebug(params);
|
||||
Message.success(t('caseManagement.featureCase.associatedSuccess'));
|
||||
loadBugList();
|
||||
showLinkBugDrawer.value = false;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
drawerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
loadBugList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.bug-item {
|
||||
@apply cursor-pointer;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: white;
|
||||
&:hover {
|
||||
@apply relative;
|
||||
|
||||
background: var(--color-text-n9);
|
||||
box-shadow: inset 0 0 0.5px 0.5px rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,251 @@
|
|||
<template>
|
||||
<div class="h-full pl-[16px]">
|
||||
<div class="mb-[16px] 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>
|
||||
<div class="comment-container">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsEmpty from '@/components/pure/ms-empty/index.vue';
|
||||
import MsComment from '@/components/business/ms-comment/comment';
|
||||
import inputComment from '@/components/business/ms-comment/input.vue';
|
||||
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
|
||||
import ReviewCommentList from '@/views/case-management/caseManagementFeature/components/tabContent/tabComment/reviewCommentList.vue';
|
||||
|
||||
import {
|
||||
addOrUpdateCommentList,
|
||||
createCommentList,
|
||||
deleteCommentList,
|
||||
editorUploadFile,
|
||||
getCommentList,
|
||||
getReviewCommentList,
|
||||
getTestPlanExecuteCommentList,
|
||||
} from '@/api/modules/case-management/featureCase';
|
||||
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
|
||||
const props = defineProps<{
|
||||
activeCase: Record<string, any>;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
async function handleUploadImage(file: File) {
|
||||
const { data } = await editorUploadFile({
|
||||
fileList: [file],
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
const activeComment = ref('caseComment');
|
||||
const commentTypeOptions = [
|
||||
{
|
||||
label: t('caseManagement.featureCase.caseComment'),
|
||||
value: 'caseComment',
|
||||
},
|
||||
{
|
||||
label: t('caseManagement.featureCase.reviewComment'),
|
||||
value: 'reviewComment',
|
||||
},
|
||||
{
|
||||
label: t('caseManagement.featureCase.executiveReview'),
|
||||
value: 'executiveComment',
|
||||
},
|
||||
];
|
||||
const commentList = ref<CommentItem[]>([]);
|
||||
const reviewCommentList = ref<CommentItem[]>([]);
|
||||
|
||||
/**
|
||||
* 初始化评论列表
|
||||
*/
|
||||
async function initCommentList() {
|
||||
try {
|
||||
const result = await getCommentList(props.activeCase.id);
|
||||
commentList.value = result;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化评审评论
|
||||
*/
|
||||
async function initReviewCommentList() {
|
||||
try {
|
||||
const result = await getReviewCommentList(props.activeCase.id);
|
||||
reviewCommentList.value = result;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化执行评论
|
||||
*/
|
||||
async function initTestPlanExecuteCommentList() {
|
||||
try {
|
||||
const result = await getTestPlanExecuteCommentList(props.activeCase.id);
|
||||
reviewCommentList.value = result;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllCommentList() {
|
||||
switch (activeComment.value) {
|
||||
case 'caseComment':
|
||||
await initCommentList();
|
||||
break;
|
||||
case 'reviewComment':
|
||||
await initReviewCommentList();
|
||||
break;
|
||||
case 'executiveComment':
|
||||
await initTestPlanExecuteCommentList();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加或者更新评论
|
||||
*/
|
||||
async function handleUpdateOrAdd(item: CommentParams, cb: (result: boolean) => void) {
|
||||
try {
|
||||
await addOrUpdateCommentList(item);
|
||||
getAllCommentList();
|
||||
cb(true);
|
||||
} catch (error) {
|
||||
cb(false);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*/
|
||||
async function handleDelete(caseCommentId: string) {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('ms.comment.deleteConfirm'),
|
||||
content: t('ms.comment.deleteContent'),
|
||||
okText: t('common.confirmDelete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
await deleteCommentList(caseCommentId);
|
||||
Message.success(t('common.deleteSuccess'));
|
||||
getAllCommentList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
const commentInputRef = ref<InstanceType<typeof inputComment>>();
|
||||
const content = ref('');
|
||||
const isActive = ref<boolean>(false);
|
||||
|
||||
const noticeUserIds = ref<string[]>([]);
|
||||
|
||||
/**
|
||||
* 发布评论
|
||||
* @param currentContent 当前评论内容
|
||||
*/
|
||||
async function publishHandler(currentContent: string) {
|
||||
try {
|
||||
const params: CommentParams = {
|
||||
caseId: props.activeCase.id,
|
||||
notifier: noticeUserIds.value.join(';'),
|
||||
replyUser: '',
|
||||
parentId: '',
|
||||
content: currentContent,
|
||||
event: noticeUserIds.value.join(';') ? 'AT' : 'COMMENT', // 任务事件(仅评论: ’COMMENT‘; 评论并@: ’AT‘; 回复评论/回复并@: ’REPLAY‘;)
|
||||
};
|
||||
await createCommentList(params);
|
||||
getAllCommentList();
|
||||
Message.success(t('common.publishSuccessfully'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPublish() {
|
||||
isActive.value = !isActive.value;
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
getAllCommentList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.commentWrapper) {
|
||||
right: 0;
|
||||
}
|
||||
.comment-container {
|
||||
.ms-scroll-bar();
|
||||
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 130px);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,621 @@
|
|||
<template>
|
||||
<MsMinderEditor
|
||||
v-model:activeExtraKey="activeExtraKey"
|
||||
v-model:extra-visible="extraVisible"
|
||||
v-model:loading="loading"
|
||||
:tags="[]"
|
||||
:import-json="importJson"
|
||||
:replaceable-tags="replaceableTags"
|
||||
:insert-node="insertNode"
|
||||
:priority-disable-check="priorityDisableCheck"
|
||||
:after-tag-edit="afterTagEdit"
|
||||
:extract-content-tab-list="extractContentTabList"
|
||||
single-tag
|
||||
tag-enable
|
||||
sequence-enable
|
||||
@node-select="handleNodeSelect"
|
||||
@save="handleMinderSave"
|
||||
>
|
||||
<template #extractTabContent>
|
||||
<baseInfo v-if="activeExtraKey === 'baseInfo'" :loading="baseInfoLoading" :active-case="activeCase" />
|
||||
<attachment
|
||||
v-else-if="activeExtraKey === 'attachment'"
|
||||
v-model:model-value="fileList"
|
||||
:active-case="activeCase"
|
||||
@upload-success="initCaseDetail"
|
||||
/>
|
||||
<caseCommentList v-else-if="activeExtraKey === 'comments'" :active-case="activeCase" />
|
||||
<bugList v-else :active-case="activeCase" />
|
||||
</template>
|
||||
</MsMinderEditor>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormItem } from '@/components/pure/ms-form-create/types';
|
||||
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
|
||||
import type { MinderJson, MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
|
||||
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||
import attachment from './attachment.vue';
|
||||
import baseInfo from './basInfo.vue';
|
||||
import bugList from './bugList.vue';
|
||||
import caseCommentList from './commentList.vue';
|
||||
|
||||
import {
|
||||
checkFileIsUpdateRequest,
|
||||
getCaseDetail,
|
||||
getCaseMinder,
|
||||
getCaseModuleTree,
|
||||
saveCaseMinder,
|
||||
} from '@/api/modules/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getGenerateId, mapTree } from '@/utils';
|
||||
|
||||
import { TableQueryParams } from '@/models/common';
|
||||
|
||||
import { convertToFile, initFormCreate } from '@/views/case-management/caseManagementFeature/components/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
modulesCount: Record<string, number>; // 模块数量
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
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,
|
||||
});
|
||||
importJson.value.root.children = res;
|
||||
importJson.value.root.data = {
|
||||
id: props.moduleId === 'all' ? '' : props.moduleId,
|
||||
text: props.moduleName,
|
||||
resource: [t('common.module')],
|
||||
};
|
||||
window.minder.importJson(importJson.value);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.moduleId === 'all') {
|
||||
initCaseTree();
|
||||
} else {
|
||||
initMinder();
|
||||
}
|
||||
});
|
||||
|
||||
async function handleMinderSave(data: any) {
|
||||
try {
|
||||
await saveCaseMinder({
|
||||
projectId: appStore.currentProjectId,
|
||||
versionId: '',
|
||||
updateCaseList: data,
|
||||
updateModuleList: [],
|
||||
deleteResourceList: [],
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已选中节点的可替换标签判断
|
||||
* @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
|
||||
? topTags.filter((tag) => !node.data?.resource?.includes(tag))
|
||||
: [];
|
||||
}
|
||||
if (node.data?.resource?.some((e) => descTags.includes(e))) {
|
||||
// 选中节点属于描述节点,可替换为除自身外的描述标签
|
||||
if (
|
||||
node.data.resource.includes(t('ms.minders.stepDesc')) &&
|
||||
(node.parent?.children?.filter((e) => e.data?.resource?.includes(t('ms.minders.stepDesc'))) || []).length > 1
|
||||
) {
|
||||
// 如果当前节点是步骤描述,则需要判断是否有其他步骤描述节点,如果有,则不可替换为文本描述
|
||||
return [];
|
||||
}
|
||||
return descTags.filter((tag) => !node.data?.resource?.includes(tag));
|
||||
}
|
||||
if (
|
||||
(!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)))
|
||||
) {
|
||||
// 选中节点无标签,且父节点为顶级节点,可替换为顶级标签
|
||||
// 如果选中节点子级含有用例节点或模块节点,则不可将选中节点标记为用例
|
||||
return node.children &&
|
||||
(node.children.some((e) => e.data?.resource?.includes(caseTag)) ||
|
||||
node.children.some((e) => e.data?.resource?.includes(moduleTag)))
|
||||
? topTags.filter((e) => e !== caseTag)
|
||||
: topTags;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行插入节点
|
||||
* @param command 插入命令
|
||||
* @param node 目标节点
|
||||
*/
|
||||
function execInert(command: string, node?: MinderJsonNodeData) {
|
||||
if (window.minder.queryCommandState(command) !== -1) {
|
||||
window.minder.execCommand(command, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入前置条件
|
||||
* @param node 目标节点
|
||||
* @param type 插入类型
|
||||
*/
|
||||
function inertPrecondition(node: MinderJsonNode, type: string) {
|
||||
const child: MinderJsonNode = {
|
||||
parent: node,
|
||||
data: {
|
||||
id: getGenerateId(),
|
||||
text: t('ms.minders.precondition'),
|
||||
resource: [t('ms.minders.precondition')],
|
||||
expandState: 'expand',
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
const sibling = {
|
||||
parent: child,
|
||||
data: {
|
||||
id: getGenerateId(),
|
||||
text: '',
|
||||
resource: [],
|
||||
},
|
||||
};
|
||||
execInert(type, child.data);
|
||||
nextTick(() => {
|
||||
execInert('AppendChildNode', sibling.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入备注
|
||||
* @param node 目标节点
|
||||
* @param type 插入类型
|
||||
*/
|
||||
function insetRemark(node: MinderJsonNode, type: string) {
|
||||
const child = {
|
||||
parent: node,
|
||||
data: {
|
||||
id: getGenerateId(),
|
||||
text: t('common.remark'),
|
||||
resource: [t('common.remark')],
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
execInert(type, child.data);
|
||||
}
|
||||
|
||||
// function insertTextDesc(node: MinderJsonNode, type: string) {
|
||||
// const child = {
|
||||
// parent: node,
|
||||
// data: {
|
||||
// id: getGenerateId(),
|
||||
// text: t('ms.minders.textDesc'),
|
||||
// resource: [t('ms.minders.textDesc')],
|
||||
// },
|
||||
// children: [],
|
||||
// };
|
||||
// const sibling = {
|
||||
// parent: child,
|
||||
// data: {
|
||||
// id: getGenerateId(),
|
||||
// text: t('ms.minders.stepExpect'),
|
||||
// resource: [t('ms.minders.stepExpect')],
|
||||
// },
|
||||
// };
|
||||
// execInert(type, {
|
||||
// ...child,
|
||||
// children: [sibling],
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* 插入步骤描述
|
||||
* @param node 目标节点
|
||||
* @param type 插入类型
|
||||
*/
|
||||
function insetStepDesc(node: MinderJsonNode, type: string) {
|
||||
const child = {
|
||||
parent: node,
|
||||
data: {
|
||||
id: getGenerateId(),
|
||||
text: t('ms.minders.stepDesc'),
|
||||
resource: [t('ms.minders.stepDesc')],
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
const sibling = {
|
||||
parent: child,
|
||||
data: {
|
||||
id: getGenerateId(),
|
||||
text: t('ms.minders.stepExpect'),
|
||||
resource: [t('ms.minders.stepExpect')],
|
||||
},
|
||||
};
|
||||
execInert(type, child.data);
|
||||
nextTick(() => {
|
||||
execInert('AppendChildNode', sibling.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入预期结果
|
||||
* @param node 目标节点
|
||||
* @param type 插入类型
|
||||
*/
|
||||
function insertExpect(node: MinderJsonNode, type: string) {
|
||||
const child = {
|
||||
parent: node,
|
||||
data: {
|
||||
id: getGenerateId(),
|
||||
text: t('ms.minders.stepExpect'),
|
||||
resource: [t('ms.minders.stepExpect')],
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
execInert(type, child.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入节点
|
||||
* @param node 目标节点
|
||||
* @param type 插入类型
|
||||
*/
|
||||
function insertNode(node: MinderJsonNode, type: string) {
|
||||
switch (type) {
|
||||
case 'AppendChildNode':
|
||||
if (node.data?.resource?.includes(moduleTag)) {
|
||||
execInert('AppendChildNode');
|
||||
} else if (node.data?.resource?.includes(caseTag)) {
|
||||
// 给用例插入子节点
|
||||
if (!node.children || node.children.length === 0) {
|
||||
// 当前用例还没有子节点,默认添加一个前置条件
|
||||
inertPrecondition(node, type);
|
||||
} else if (node.children.length > 0) {
|
||||
// 当前用例有子节点
|
||||
let hasPreCondition = false;
|
||||
let hasTextDesc = false;
|
||||
let hasRemark = false;
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i];
|
||||
if (child.data?.resource?.includes(t('ms.minders.precondition'))) {
|
||||
hasPreCondition = true;
|
||||
} else if (child.data?.resource?.includes(t('ms.minders.textDesc'))) {
|
||||
hasTextDesc = true;
|
||||
} else if (child.data?.resource?.includes(t('common.remark'))) {
|
||||
hasRemark = true;
|
||||
}
|
||||
}
|
||||
if (!hasPreCondition) {
|
||||
// 没有前置条件,则默认添加一个前置条件
|
||||
inertPrecondition(node, type);
|
||||
} else if (!hasRemark) {
|
||||
// 没有备注,则默认添加一个备注
|
||||
insetRemark(node, type);
|
||||
} else if (!hasTextDesc) {
|
||||
// 没有文本描述,则默认添加一个步骤描述
|
||||
insetStepDesc(node, type);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(node.data?.resource?.includes(t('ms.minders.stepDesc')) ||
|
||||
node.data?.resource?.includes(t('ms.minders.textDesc'))) &&
|
||||
(!node.children || node.children.length === 0)
|
||||
) {
|
||||
// 当前节点是步骤描述或文本描述,且没有子节点,则默认添加一个预期结果
|
||||
insertExpect(node, 'AppendChildNode');
|
||||
} else if (node.data?.resource?.includes(t('ms.minders.precondition'))) {
|
||||
// 当前节点是前置条件,则默认添加一个文本节点
|
||||
execInert('AppendChildNode');
|
||||
}
|
||||
break;
|
||||
case 'AppendParentNode':
|
||||
execInert('AppendParentNode');
|
||||
break;
|
||||
case 'AppendSiblingNode':
|
||||
if (node.parent?.data?.resource?.includes(caseTag) && node.parent?.children) {
|
||||
// 当前节点的父节点是用例
|
||||
let hasPreCondition = false;
|
||||
let hasTextDesc = false;
|
||||
let hasRemark = false;
|
||||
for (let i = 0; i < node.parent.children.length; i++) {
|
||||
const sibling = node.parent.children[i];
|
||||
if (sibling.data?.resource?.includes(t('ms.minders.precondition'))) {
|
||||
hasPreCondition = true;
|
||||
} else if (sibling.data?.resource?.includes(t('common.remark'))) {
|
||||
hasRemark = true;
|
||||
} else if (sibling.data?.resource?.includes(t('ms.minders.textDesc'))) {
|
||||
hasTextDesc = true;
|
||||
}
|
||||
}
|
||||
if (!hasPreCondition) {
|
||||
// 没有前置条件,则默认添加一个前置条件
|
||||
inertPrecondition(node, type);
|
||||
} else if (!hasRemark) {
|
||||
// 没有备注,则默认添加一个备注
|
||||
insetRemark(node, type);
|
||||
} else if (!hasTextDesc) {
|
||||
// 没有文本描述,则默认添加一个步骤描述
|
||||
insetStepDesc(node, type);
|
||||
}
|
||||
} else if (node.parent?.data?.resource?.includes(moduleTag) || !node.parent?.data?.resource) {
|
||||
// 当前节点的父节点是模块或没有标签,则默认添加一个文本节点
|
||||
execInert('AppendSiblingNode');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否可打优先级
|
||||
*/
|
||||
function priorityDisableCheck(node: MinderJsonNode) {
|
||||
if (node.data?.resource?.includes(caseTag)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签编辑后,如果将标签修改为模块,则删除已添加的优先级
|
||||
* @param node 选中节点
|
||||
* @param tag 更改后的标签
|
||||
*/
|
||||
function afterTagEdit(node: MinderJsonNode, tag: string) {
|
||||
if (tag === moduleTag && node.data) {
|
||||
window.minder.execCommand('priority');
|
||||
}
|
||||
}
|
||||
|
||||
const baseInfoLoading = ref(false);
|
||||
|
||||
const formRules = ref<FormItem[]>([]);
|
||||
|
||||
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');
|
||||
|
||||
const fileList = ref<MsFileItem[]>([]);
|
||||
const checkUpdateFileIds = ref<string[]>([]);
|
||||
|
||||
const getListFunParams = ref<TableQueryParams>({
|
||||
combine: {
|
||||
hiddenIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
// 监视文件列表处理关联和本地文件
|
||||
watch(
|
||||
() => fileList.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getListFunParams.value.combine.hiddenIds = fileList.value.filter((item) => !item.local).map((item) => item.uid);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 初始化用例详情
|
||||
* @param data 节点数据/用例数据
|
||||
*/
|
||||
async function initCaseDetail(data?: MinderJsonNodeData | Record<string, any>) {
|
||||
try {
|
||||
baseInfoLoading.value = true;
|
||||
const res = await getCaseDetail(data?.id || activeCase.value.id);
|
||||
activeCase.value = res;
|
||||
const fileIds = (res.attachments || []).map((item: any) => item.id) || [];
|
||||
if (fileIds.length) {
|
||||
checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds);
|
||||
}
|
||||
formRules.value = initFormCreate(res.customFields, ['FUNCTIONAL_CASE:READ+UPDATE']);
|
||||
if (res.attachments) {
|
||||
// 处理文件列表
|
||||
fileList.value = res.attachments
|
||||
.map((fileInfo: any) => {
|
||||
return {
|
||||
...fileInfo,
|
||||
name: fileInfo.fileName,
|
||||
isUpdateFlag: checkUpdateFileIds.value.includes(fileInfo.id),
|
||||
};
|
||||
})
|
||||
.map((fileInfo: any) => {
|
||||
return convertToFile(fileInfo);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
baseInfoLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetExtractInfo() {
|
||||
activeCase.value = {};
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理脑图节点激活/点击
|
||||
* @param node 被激活/点击的节点
|
||||
*/
|
||||
async function handleNodeSelect(node: MinderJsonNode) {
|
||||
const { data } = node;
|
||||
if (data?.resource && data.resource.includes(caseTag)) {
|
||||
extraVisible.value = true;
|
||||
activeExtraKey.value = 'baseInfo';
|
||||
resetExtractInfo();
|
||||
initCaseDetail(data);
|
||||
} 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); // 移除占位的虚拟节点
|
||||
if (fakeNode) {
|
||||
window.minder.removeNode(fakeNode);
|
||||
}
|
||||
if ((!res || res.length === 0) && node.children?.length) {
|
||||
// 如果模块下没有用例且有别的模块节点,正常展开
|
||||
node.expand();
|
||||
node.renderTree();
|
||||
window.minder.layout();
|
||||
return;
|
||||
}
|
||||
// TODO:递归渲染存在的子节点
|
||||
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.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;
|
||||
resetExtractInfo();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -3,7 +3,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FeatureCaseMinder from '@/components/business/ms-minders/featureCaseMinder.vue';
|
||||
import FeatureCaseMinder from './featureCaseMinder/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
minderType: 'FeatureCase';
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
export default {
|
||||
'ms.minders.allModule': 'All Modules',
|
||||
'ms.minders.precondition': 'Precondition',
|
||||
'ms.minders.stepDesc': 'Step Description',
|
||||
'ms.minders.stepExpect': 'Expected Result',
|
||||
'ms.minders.textDesc': 'Text Description',
|
||||
'ms.minders.caseName': 'Test Case Name',
|
||||
'ms.minders.caseNameNotNull': 'Test Case Name cannot be empty',
|
||||
'ms.minders.commentTotal': '{num} Comments in Total',
|
||||
};
|
||||
|
|
|
@ -45,15 +45,15 @@
|
|||
</div>
|
||||
<slot name="itemRight" :item="item" :index="index"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.mode === 'remote' && index === props.data.length - 1"
|
||||
class="flex h-[32px] items-center justify-center"
|
||||
>
|
||||
<div v-if="noMoreData" class="text-[var(--color-text-4)]">{{ t('ms.timeline.noMoreData') }}</div>
|
||||
<a-spin v-else />
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
<div
|
||||
v-if="props.mode === 'remote' && index === props.data.length - 1"
|
||||
class="flex h-[32px] items-center justify-center"
|
||||
>
|
||||
<div v-if="noMoreData" class="text-[var(--color-text-4)]">{{ t('ms.timeline.noMoreData') }}</div>
|
||||
<a-spin v-else />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="$slots['empty'] || props.emptyText" #empty>
|
||||
<slot name="empty">
|
||||
|
|
|
@ -63,6 +63,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" name="minderEditor" setup>
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||
import minderHeader from './main/header.vue';
|
||||
import mainEditor from './main/mainEditor.vue';
|
||||
|
@ -87,7 +89,7 @@
|
|||
(e: 'save', data: Record<string, any>): void;
|
||||
(e: 'afterMount'): void;
|
||||
(e: 'enterNode', data: any): void;
|
||||
(e: 'nodeClick', data: any): void;
|
||||
(e: 'nodeSelect', data: any): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -133,15 +135,15 @@
|
|||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (window.minder.on) {
|
||||
window.minder.on('mousedown', (e: any) => {
|
||||
if (e.originEvent.button === 0) {
|
||||
// 鼠标左键点击
|
||||
window.minder.on(
|
||||
'selectionchange',
|
||||
debounce(() => {
|
||||
const selectedNode: MinderJsonNode = window.minder.getSelectedNode();
|
||||
if (Object.keys(window.minder).length > 0 && selectedNode) {
|
||||
emit('nodeClick', selectedNode);
|
||||
emit('nodeSelect', selectedNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 300)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ export enum TableModuleEnum {
|
|||
export enum TableKeyEnum {
|
||||
API_TEST = 'apiTest',
|
||||
API_TEST_MANAGEMENT_CASE = 'apiTestManagementCase',
|
||||
API_TEST_MANAGEMENT_MOCK = 'apiTestManagementMock',
|
||||
API_TEST_DEBUG_FORM_DATA = 'apiTestDebugFormData',
|
||||
API_TEST_DEBUG_REST = 'apiTestDebugRest',
|
||||
API_TEST_DEBUG_QUERY = 'apiTestDebugQuery',
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
v-on="propsEvent"
|
||||
@selected-change="handleTableSelect"
|
||||
@batch-action="handleTableBatch"
|
||||
@module-change="loadMockList()"
|
||||
>
|
||||
<template #expectNum="{ record }">
|
||||
<MsButton type="text" @click="handleOpenDetail(record)">
|
||||
|
@ -336,7 +337,7 @@
|
|||
{
|
||||
columns: props.readOnly ? columns : [],
|
||||
scroll: { x: '100%' },
|
||||
tableKey: props.readOnly ? undefined : TableKeyEnum.API_TEST,
|
||||
tableKey: props.readOnly ? undefined : TableKeyEnum.API_TEST_MANAGEMENT_MOCK,
|
||||
showSetting: !props.readOnly,
|
||||
selectable: true,
|
||||
showSelectAll: !props.readOnly,
|
||||
|
@ -384,7 +385,7 @@
|
|||
let moduleIds: string[] = [];
|
||||
if (props.activeModule !== 'all') {
|
||||
moduleIds = [props.activeModule];
|
||||
const getAllChildren = await tableStore.getSubShow(TableKeyEnum.API_TEST_MANAGEMENT_CASE);
|
||||
const getAllChildren = await tableStore.getSubShow(TableKeyEnum.API_TEST_MANAGEMENT_MOCK);
|
||||
if (getAllChildren) {
|
||||
moduleIds = [props.activeModule, ...props.offspringIds];
|
||||
}
|
||||
|
@ -705,7 +706,7 @@
|
|||
});
|
||||
|
||||
if (!props.readOnly) {
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_MOCK, columns, 'drawer');
|
||||
} else {
|
||||
columns = columns.filter(
|
||||
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
|
||||
|
|
|
@ -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]">
|
||||
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-radio-group> -->
|
||||
</template>
|
||||
</MsAdvanceFilter>
|
||||
<ms-base-table
|
||||
|
|
Loading…
Reference in New Issue