feat(功能用例): 用例评审脑图操作-评审
This commit is contained in:
parent
d5b320ae99
commit
153a5a3860
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -135,4 +135,5 @@ export default {
|
|||
'caseManagement.caseReview.updateCase': '更新用例',
|
||||
'caseManagement.caseReview.reviewSuccess.widthAdmin':
|
||||
'提交成功! 您不是当前项目指定的评审人,系统只会记录您的评审,不影响最终评审结果',
|
||||
'caseManagement.caseReview.reviewFormTip': '添加操作人的评审结果,多人评审需所有评审人评审通过',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue