refactor(用例脑图): 用例脑图代码结构拆分&mock 表格显示子模块设置

This commit is contained in:
baiqi 2024-05-23 18:37:40 +08:00 committed by 刘瑞斌
parent 6619efe9a3
commit c89e7902f9
14 changed files with 1624 additions and 1467 deletions

View File

@ -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)',
}"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -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',
};

View File

@ -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">

View File

@ -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)
);
}
});
});

View File

@ -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',

View File

@ -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)

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]">
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio>
</a-radio-group>
</a-radio-group> -->
</template>
</MsAdvanceFilter>
<ms-base-table