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 v-if="showCaseMenu">
<!-- 评审 查看详情 -->
<a-tooltip :content="t('caseManagement.caseReview.review')">
<MsButton type="icon" class="ms-minder-node-float-menu-icon-button">
<MsIcon type="icon-icon_audit" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<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')">
<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)]" />
</MsButton>
</a-tooltip>
<template #content>
<div class="w-[440px] rounded bg-white p-[16px] shadow-[0_0_10px_rgba(0,0,0,0.05)]">
<ReviewSubmit
:review-pass-rule="reviewPassRule"
:case-id="selectCaseId"
: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" @click="toggleDetail">
<MsIcon
type="icon-icon_describe_outlined"
class="text-[var(--color-text-4)]"
:class="[extraVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
/>
<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>
</a-tooltip>
</template>
@ -104,15 +132,27 @@
import Attachment from '@/components/business/ms-minders/featureCaseMinder/attachment.vue';
import ReviewCommentList from '@/views/case-management/caseManagementFeature/components/tabContent/tabComment/reviewCommentList.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 { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store';
import useAppStore from '@/store/modules/app';
import useMinderStore from '@/store/modules/components/minder-editor/index';
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 { MinderEventName } from '@/enums/minderEnum';
@ -130,12 +170,14 @@
const emit = defineEmits<{
(e: 'operation', type: string, data: MinderJsonNodeData): void;
(e: 'handleReviewDone'): void;
}>();
const route = useRoute();
const appStore = useAppStore();
const { t } = useI18n();
const minderStore = useMinderStore();
const userStore = useUserStore();
const statusTagMap: Record<string, string> = {
PASS: t('common.pass'),
@ -474,6 +516,8 @@
}
const canShowFloatMenu = ref(false); //
const isReviewer = ref(false); //
const caseReviewerList = ref<CaseReviewFunctionalCaseUserItem[]>([]);
const canShowEnterNode = ref(false);
const showCaseMenu = ref(false);
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 节点
@ -538,7 +613,9 @@
if (data?.resource?.includes(caseTag)) {
showCaseMenu.value = true;
selectCaseId.value = node.data?.caseId ?? '';
setMoreMenuOtherOperationList(node.data as MinderJsonNodeData);
setIsReviewer(node.data);
if (extraVisible.value) {
toggleDetail(true);
}

View File

@ -12,6 +12,13 @@ export enum StatusType {
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 {
PENDING = 'PENDING',
SUCCESS = 'SUCCESS',

View File

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

View File

@ -143,6 +143,7 @@
:review-progress="props.reviewProgress"
:review-pass-rule="props.reviewPassRule"
@operation="handleMinderOperation"
@handle-review-done="emitRefresh"
/>
</div>
<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(
() => props.onlyMine,
() => {
@ -641,13 +661,7 @@
try {
disassociateLoading.value = true;
await disassociateReviewCase(route.query.id as string, record.caseId);
emit('refresh', {
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
emitRefresh();
if (done) {
done();
}
@ -719,14 +733,8 @@
});
Message.success(t('common.updateSuccess'));
dialogLoading.value = false;
refresh();
emit('refresh', {
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
refresh(false);
emitRefresh();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -787,13 +795,7 @@
Message.success(t('common.updateSuccess'));
dialogVisible.value = false;
refresh(false);
emit('refresh', {
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
emitRefresh();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -860,13 +862,7 @@
Message.success(t('caseManagement.caseReview.reviewSuccess'));
dialogVisible.value = false;
resetSelector();
emit('refresh', {
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
emitRefresh();
loadList();
} catch (error) {
// 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.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.',
'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.reviewSuccess.widthAdmin':
'提交成功! 您不是当前项目指定的评审人,系统只会记录您的评审,不影响最终评审结果',
'caseManagement.caseReview.reviewFormTip': '添加操作人的评审结果,多人评审需所有评审人评审通过',
};