feat(功能用例): 用例评审脑图操作-评审

This commit is contained in:
teukkk 2024-07-08 17:20:46 +08:00 committed by Craftsman
parent d5b320ae99
commit 153a5a3860
8 changed files with 333 additions and 46 deletions

View File

@ -19,18 +19,46 @@
<template #extractMenu> <template #extractMenu>
<template v-if="showCaseMenu"> <template v-if="showCaseMenu">
<!-- 评审 查看详情 --> <!-- 评审 查看详情 -->
<a-trigger
v-if="hasAnyPermission(['CASE_REVIEW:READ+REVIEW']) && isReviewer"
v-model:popup-visible="reviewVisible"
trigger="click"
position="bl"
:click-outside-to-close="false"
popup-container=".ms-minder-container"
>
<a-tooltip :content="t('caseManagement.caseReview.review')"> <a-tooltip :content="t('caseManagement.caseReview.review')">
<MsButton type="icon" class="ms-minder-node-float-menu-icon-button"> <MsButton
type="icon"
:class="[
'ms-minder-node-float-menu-icon-button',
`${reviewVisible ? 'ms-minder-node-float-menu-icon-button--focus' : ''}`,
]"
>
<MsIcon type="icon-icon_audit" class="text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_audit" class="text-[var(--color-text-4)]" />
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<a-tooltip :content="t('common.detail')"> <template #content>
<MsButton type="icon" class="ms-minder-node-float-menu-icon-button" @click="toggleDetail"> <div class="w-[440px] rounded bg-white p-[16px] shadow-[0_0_10px_rgba(0,0,0,0.05)]">
<MsIcon <ReviewSubmit
type="icon-icon_describe_outlined" :review-pass-rule="reviewPassRule"
class="text-[var(--color-text-4)]" :case-id="selectCaseId"
:class="[extraVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']" :review-id="route.query.id as string"
@done="handleReviewDone"
/> />
</div>
</template>
</a-trigger>
<a-tooltip :content="t('common.detail')">
<MsButton
type="icon"
:class="[
'ms-minder-node-float-menu-icon-button',
`${extraVisible ? 'ms-minder-node-float-menu-icon-button--focus' : ''}`,
]"
@click="toggleDetail"
>
<MsIcon type="icon-icon_describe_outlined" class="text-[var(--color-text-4)]" />
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
</template> </template>
@ -104,15 +132,27 @@
import Attachment from '@/components/business/ms-minders/featureCaseMinder/attachment.vue'; import Attachment from '@/components/business/ms-minders/featureCaseMinder/attachment.vue';
import ReviewCommentList from '@/views/case-management/caseManagementFeature/components/tabContent/tabComment/reviewCommentList.vue'; import ReviewCommentList from '@/views/case-management/caseManagementFeature/components/tabContent/tabComment/reviewCommentList.vue';
import ReviewResult from '@/views/case-management/caseReview/components/reviewResult.vue'; import ReviewResult from '@/views/case-management/caseReview/components/reviewResult.vue';
import ReviewSubmit from '@/views/case-management/caseReview/components/reviewSubmit.vue';
import { getCaseReviewHistoryList, getCaseReviewMinder } from '@/api/modules/case-management/caseReview'; import {
getCaseReviewerList,
getCaseReviewHistoryList,
getCaseReviewMinder,
} from '@/api/modules/case-management/caseReview';
import { getCaseDetail } from '@/api/modules/case-management/featureCase'; import { getCaseDetail } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import useMinderStore from '@/store/modules/components/minder-editor/index'; import useMinderStore from '@/store/modules/components/minder-editor/index';
import { findNodeByKey, mapTree, replaceNodeInTree } from '@/utils'; import { findNodeByKey, mapTree, replaceNodeInTree } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { ReviewHistoryItem, ReviewPassRule } from '@/models/caseManagement/caseReview'; import {
CaseReviewFunctionalCaseUserItem,
ReviewHistoryItem,
ReviewPassRule,
ReviewResult as ReviewResultStatus,
} from '@/models/caseManagement/caseReview';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { MinderEventName } from '@/enums/minderEnum'; import { MinderEventName } from '@/enums/minderEnum';
@ -130,12 +170,14 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'operation', type: string, data: MinderJsonNodeData): void; (e: 'operation', type: string, data: MinderJsonNodeData): void;
(e: 'handleReviewDone'): void;
}>(); }>();
const route = useRoute(); const route = useRoute();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const minderStore = useMinderStore(); const minderStore = useMinderStore();
const userStore = useUserStore();
const statusTagMap: Record<string, string> = { const statusTagMap: Record<string, string> = {
PASS: t('common.pass'), PASS: t('common.pass'),
@ -474,6 +516,8 @@
} }
const canShowFloatMenu = ref(false); // const canShowFloatMenu = ref(false); //
const isReviewer = ref(false); //
const caseReviewerList = ref<CaseReviewFunctionalCaseUserItem[]>([]);
const canShowEnterNode = ref(false); const canShowEnterNode = ref(false);
const showCaseMenu = ref(false); const showCaseMenu = ref(false);
const moreMenuOtherOperationList = ref(); const moreMenuOtherOperationList = ref();
@ -506,6 +550,37 @@
]; ];
} }
const selectCaseId = ref('');
const reviewVisible = ref(false);
function handleReviewDone(status: ReviewResultStatus) {
reviewVisible.value = false;
let origin = window.minder.queryCommandValue('resource');
if (origin[0] !== caseTag) {
origin[0] = statusTagMap[status];
} else {
origin = [statusTagMap[status], ...origin];
}
window.minder.execCommand('resource', origin);
minderStore.dispatchEvent(
MinderEventName.SET_TAG,
undefined,
undefined,
undefined,
window.minder.getSelectedNodes()
);
setPriorityView(true, 'P');
emit('handleReviewDone');
}
/**
* 是否是当前用例的评审人
* @param data 节点信息
*/
async function setIsReviewer(data?: MinderJsonNodeData) {
caseReviewerList.value = await getCaseReviewerList(route.query.id as string, data?.caseId);
isReviewer.value = caseReviewerList.value.some((child) => child.userId === userStore.id);
}
/** /**
* 处理节点选中 * 处理节点选中
* @param node 节点 * @param node 节点
@ -538,7 +613,9 @@
if (data?.resource?.includes(caseTag)) { if (data?.resource?.includes(caseTag)) {
showCaseMenu.value = true; showCaseMenu.value = true;
selectCaseId.value = node.data?.caseId ?? '';
setMoreMenuOtherOperationList(node.data as MinderJsonNodeData); setMoreMenuOtherOperationList(node.data as MinderJsonNodeData);
setIsReviewer(node.data);
if (extraVisible.value) { if (extraVisible.value) {
toggleDetail(true); toggleDetail(true);
} }

View File

@ -12,6 +12,13 @@ export enum StatusType {
PENDING = 'icon-icon_block_filled', // 未执行 PENDING = 'icon-icon_block_filled', // 未执行
} }
// 评审UNDER_REVIEWED建议PASS通过UN_PASS未通过
export enum StartReviewStatus {
PASS = 'PASS',
UN_PASS = 'UN_PASS',
UNDER_REVIEWED = 'UNDER_REVIEWED',
}
export enum LastExecuteResults { export enum LastExecuteResults {
PENDING = 'PENDING', PENDING = 'PENDING',
SUCCESS = 'SUCCESS', SUCCESS = 'SUCCESS',

View File

@ -218,15 +218,18 @@ export interface ReviewCaseItem {
moduleName: string; moduleName: string;
} }
// 评审详情-提交评审入参 // 评审详情-提交评审入参
export interface CommitReviewResultParams { export interface ReviewFormParams {
status: ReviewResult;
content: string;
notifiers?: string[];
reviewCommentFileIds?: string[];
}
export interface CommitReviewResultParams extends ReviewFormParams {
projectId: string; projectId: string;
reviewId: string; reviewId: string;
caseId: string; caseId: string;
reviewPassRule: ReviewPassRule; reviewPassRule: ReviewPassRule;
status: ReviewResult;
content: string;
notifier: string; notifier: string;
reviewCommentFileIds?: string[];
} }
// 评审详情-获取用例评审历史 // 评审详情-获取用例评审历史
export interface ReviewHistoryItem { export interface ReviewHistoryItem {

View File

@ -143,6 +143,7 @@
:review-progress="props.reviewProgress" :review-progress="props.reviewProgress"
:review-pass-rule="props.reviewPassRule" :review-pass-rule="props.reviewPassRule"
@operation="handleMinderOperation" @operation="handleMinderOperation"
@handle-review-done="emitRefresh"
/> />
</div> </div>
<a-modal <a-modal
@ -565,6 +566,25 @@
} }
} }
function emitRefresh() {
if (showType.value === 'list') {
emit('refresh', {
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
} else {
emit('refresh', {
moduleIds: [props.activeFolder],
projectId: appStore.currentProjectId,
pageSize: 10,
current: 1,
});
}
}
watch( watch(
() => props.onlyMine, () => props.onlyMine,
() => { () => {
@ -641,13 +661,7 @@
try { try {
disassociateLoading.value = true; disassociateLoading.value = true;
await disassociateReviewCase(route.query.id as string, record.caseId); await disassociateReviewCase(route.query.id as string, record.caseId);
emit('refresh', { emitRefresh();
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
if (done) { if (done) {
done(); done();
} }
@ -719,14 +733,8 @@
}); });
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogLoading.value = false; dialogLoading.value = false;
refresh(); refresh(false);
emit('refresh', { emitRefresh();
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -787,13 +795,7 @@
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
refresh(false); refresh(false);
emit('refresh', { emitRefresh();
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -860,13 +862,7 @@
Message.success(t('caseManagement.caseReview.reviewSuccess')); Message.success(t('caseManagement.caseReview.reviewSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
resetSelector(); resetSelector();
emit('refresh', { emitRefresh();
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
loadList(); loadList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -0,0 +1,76 @@
<template>
<a-form :model="form">
<a-form-item field="lastExecResult" class="mb-[8px]">
<a-radio-group v-model:model-value="form.status" @change="clearContent">
<a-radio v-for="item in StartReviewStatus" :key="item" :value="item">
<ReviewResult :status="item" is-part />
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="content" asterisk-position="end" class="mb-0">
<div class="flex w-full items-center">
<MsRichText
v-model:raw="form.content"
v-model:commentIds="form.notifiers"
v-model:filedIds="form.reviewCommentFileIds"
:upload-image="handleUploadImage"
:preview-url="PreviewEditorImageUrl"
:auto-height="false"
class="w-full"
:placeholder="
props.isDblclickPlaceholder
? t('testPlan.featureCase.richTextDblclickPlaceholder')
: t('editor.placeholder')
"
/>
</div>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import ReviewResult from './reviewResult.vue';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { ReviewFormParams } from '@/models/caseManagement/caseReview';
import { StartReviewStatus } from '@/enums/caseEnum';
const props = defineProps<{
isDblclickPlaceholder?: boolean;
}>();
const form = defineModel<ReviewFormParams>('form', {
required: true,
});
const { t } = useI18n();
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
function clearContent() {
form.value = {
content: '',
reviewCommentFileIds: [] as string[],
notifiers: [] as string[],
status: form.value.status,
};
}
</script>
<style lang="less" scoped>
:deep(.arco-form-item-label-col) {
display: none;
}
:deep(.arco-form-item-wrapper-col) {
flex: 1;
}
</style>

View File

@ -0,0 +1,125 @@
<template>
<ReviewForm v-model:form="form" is-dblclick-placeholder class="execute-form" />
<div v-show="props.reviewPassRule === 'MULTIPLE'" class="mt-[4px] text-[12px] text-[var(--color-text-4)]">{{
t('caseManagement.caseReview.reviewFormTip')
}}</div>
<a-button
type="primary"
class="mt-[12px]"
:loading="submitLoading"
:disabled="submitDisabled"
@click="() => submit()"
>
{{ t('caseManagement.caseReview.commitResult') }}
</a-button>
<a-modal
v-model:visible="modalVisible"
:title="t('testPlan.featureCase.startExecution')"
class="p-[4px]"
title-align="start"
body-class="p-0"
:width="800"
:cancel-button-props="{ disabled: submitLoading }"
:ok-loading="submitLoading"
:ok-text="t('caseManagement.caseReview.commitResult')"
:ok-button-props="{ disabled: submitDisabled }"
@before-ok="submit"
>
<ReviewForm v-model:form="form" />
</a-modal>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue';
import { useEventListener } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import ReviewForm from './reviewFormRichText.vue';
import { saveCaseReviewResult } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ReviewFormParams, ReviewPassRule, ReviewResult } from '@/models/caseManagement/caseReview';
import { StartReviewStatus } from '@/enums/caseEnum';
const props = defineProps<{
caseId: string;
reviewPassRule: ReviewPassRule;
reviewId: string;
}>();
const emit = defineEmits<{
(e: 'done', status: ReviewResult): void;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const defaultForm: ReviewFormParams = {
status: StartReviewStatus.PASS,
content: '',
reviewCommentFileIds: [] as string[],
notifiers: [] as string[],
};
const form = ref({ ...defaultForm });
const modalVisible = ref(false);
const submitLoading = ref(false);
const submitDisabled = computed(
() =>
form.value.status !== StartReviewStatus.PASS &&
(form.value.content === '' || form.value.content.trim() === '<p style=""></p>')
);
//
onMounted(() => {
nextTick(() => {
const editorContent = document.querySelector('.execute-form')?.querySelector('.editor-content');
useEventListener(editorContent, 'dblclick', () => {
modalVisible.value = true;
});
});
});
watch(
() => props.caseId,
() => {
form.value = { ...defaultForm };
}
);
//
async function submit() {
try {
submitLoading.value = true;
const params = {
projectId: appStore.currentProjectId,
caseId: props.caseId,
reviewId: props.reviewId,
reviewPassRule: props.reviewPassRule,
...form.value,
notifier: form.value.notifiers?.join(';') ?? '',
};
await saveCaseReviewResult(params);
modalVisible.value = false;
Message.success(t('caseManagement.caseReview.reviewSuccess'));
emit('done', form.value.status);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
submitLoading.value = false;
}
}
</script>
<style lang="less" scoped>
.execute-form :deep(.rich-wrapper) .halo-rich-text-editor .editor-content {
max-height: 54px !important;
.ProseMirror {
min-height: 38px;
}
}
</style>

View File

@ -136,4 +136,6 @@ export default {
'caseManagement.caseReview.updateCase': 'update case', 'caseManagement.caseReview.updateCase': 'update case',
'caseManagement.caseReview.reviewSuccess.widthAdmin': 'caseManagement.caseReview.reviewSuccess.widthAdmin':
'Submitted successfully! You are not the designated reviewer for the current project. The system will only record your review and will not affect the final review result.', 'Submitted successfully! You are not the designated reviewer for the current project. The system will only record your review and will not affect the final review result.',
'caseManagement.caseReview.reviewFormTip':
'Add the review results of the operator, and multiple reviewers must pass the review by all reviewers',
}; };

View File

@ -135,4 +135,5 @@ export default {
'caseManagement.caseReview.updateCase': '更新用例', 'caseManagement.caseReview.updateCase': '更新用例',
'caseManagement.caseReview.reviewSuccess.widthAdmin': 'caseManagement.caseReview.reviewSuccess.widthAdmin':
'提交成功! 您不是当前项目指定的评审人,系统只会记录您的评审,不影响最终评审结果', '提交成功! 您不是当前项目指定的评审人,系统只会记录您的评审,不影响最终评审结果',
'caseManagement.caseReview.reviewFormTip': '添加操作人的评审结果,多人评审需所有评审人评审通过',
}; };