feat(用例评审): 用例评审Done

This commit is contained in:
baiqi 2024-02-02 15:26:48 +08:00 committed by Craftsman
parent f83f94bac2
commit 067e7dc689
6 changed files with 174 additions and 162 deletions

View File

@ -145,25 +145,21 @@
const cardOverHeight = computed(() => { const cardOverHeight = computed(() => {
if (isFullScreen.value) { if (isFullScreen.value) {
if (props.simple) {
//
return props.noContentPadding ? 0 : 48;
}
if (props.hideFooter) { if (props.hideFooter) {
// //
return props.noContentPadding ? 88 : 118; return 62;
} }
return 246; return 142;
} }
if (props.simple) { if (props.simple) {
// //
return props.noContentPadding ? 88 + _specialHeight : 136 + _specialHeight; return props.noContentPadding ? 76 + _specialHeight : 124 + _specialHeight;
} }
if (props.hideFooter) { if (props.hideFooter) {
// //
return props.noContentPadding ? 152 + _specialHeight : 192 + _specialHeight; return props.noContentPadding ? 140 + _specialHeight : 180 + _specialHeight;
} }
return 246 + _specialHeight; return 234 + _specialHeight;
}); });
const getComputedContentStyle = computed(() => { const getComputedContentStyle = computed(() => {

View File

@ -517,6 +517,8 @@
:deep(.halo-rich-text-editor) { :deep(.halo-rich-text-editor) {
padding: 16px 24px !important; padding: 16px 24px !important;
.editor-header { .editor-header {
.ms-scroll-bar();
justify-content: start !important; justify-content: start !important;
} }
p:first-child { p:first-child {

View File

@ -48,7 +48,7 @@
<template v-else> <template v-else>
<div class="ms-upload-main-text w-full"> <div class="ms-upload-main-text w-full">
<a-tooltip :content="fileList[0]?.name"> <a-tooltip :content="fileList[0]?.name">
<span class="one-line-text w-[80%]"> {{ fileList[0]?.name }}</span> <span class="one-line-text w-[80%] text-center"> {{ fileList[0]?.name }}</span>
</a-tooltip> </a-tooltip>
</div> </div>
<div class="ms-upload-sub-text">{{ formatFileSize(fileList[0]?.file?.size || 0) }}</div> <div class="ms-upload-sub-text">{{ formatFileSize(fileList[0]?.file?.size || 0) }}</div>

View File

@ -166,14 +166,14 @@
</div> </div>
<caseTabDemand <caseTabDemand
ref="caseDemandRef" ref="caseDemandRef"
:fun-params="{ projectId: appStore.currentProjectId, caseId: route.query.caseId as string, keyword: demandKeyword }" :fun-params="{ projectId: appStore.currentProjectId, caseId: activeCaseId, keyword: demandKeyword }"
:show-empty="false" :show-empty="false"
/> />
</div> </div>
<div v-else class="flex h-full flex-col overflow-hidden"> <div v-else class="flex flex-1 flex-col overflow-hidden">
<div class="review-history-list"> <div class="review-history-list">
<a-spin :loading="reviewHistoryListLoading" class="h-full w-full"> <a-spin :loading="reviewHistoryListLoading" class="h-full w-full">
<div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]"> <div v-for="item of reviewHistoryList" :key="item.id" class="review-history-list-item">
<div class="flex items-center"> <div class="flex items-center">
<MSAvatar :avatar="item.userLogo" /> <MSAvatar :avatar="item.userLogo" />
<div class="ml-[8px] flex items-center"> <div class="ml-[8px] flex items-center">
@ -208,7 +208,7 @@
</div> </div>
</div> </div>
<div class="content-footer"> <div class="content-footer">
<div class="mb-[16px] flex items-center"> <div class="mb-[12px] flex items-center">
<div class="font-medium text-[var(--color-text-1)]"> <div class="font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.startReview') }} {{ t('caseManagement.caseReview.startReview') }}
</div> </div>
@ -225,10 +225,12 @@
/> />
</a-tooltip> </a-tooltip>
</div> </div>
<reviewForm ref="dialogFormRef" /> <reviewForm
<a-button type="primary" class="mt-[16px]" :loading="submitReviewLoading" @click="submitReview"> :review-id="reviewId"
{{ t('caseManagement.caseReview.submitReview') }} :case-id="activeCaseId"
</a-button> :review-pass-rule="reviewDetail.reviewPassRule"
@done="reviewDone"
/>
</div> </div>
</a-spin> </a-spin>
</div> </div>
@ -250,7 +252,6 @@
* @description 功能测试-用例评审-用例详情 * @description 功能测试-用例评审-用例详情
*/ */
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MSAvatar from '@/components/pure/ms-avatar/index.vue'; import MSAvatar from '@/components/pure/ms-avatar/index.vue';
@ -260,7 +261,6 @@
import MsEmpty from '@/components/pure/ms-empty/index.vue'; import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPagination from '@/components/pure/ms-pagination/index'; import MsPagination from '@/components/pure/ms-pagination/index';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import caseTemplateDetail from '../caseManagementFeature/components/caseTemplateDetail.vue'; import caseTemplateDetail from '../caseManagementFeature/components/caseTemplateDetail.vue';
@ -272,7 +272,6 @@
getCaseReviewHistoryList, getCaseReviewHistoryList,
getReviewDetail, getReviewDetail,
getReviewDetailCasePage, getReviewDetailCasePage,
saveCaseReviewResult,
} from '@/api/modules/case-management/caseReview'; } from '@/api/modules/case-management/caseReview';
import { getCaseDetail } from '@/api/modules/case-management/featureCase'; import { getCaseDetail } from '@/api/modules/case-management/featureCase';
import { reviewDefaultDetail, reviewResultMap } from '@/config/caseManagement'; import { reviewDefaultDetail, reviewResultMap } from '@/config/caseManagement';
@ -283,21 +282,20 @@
import type { DetailCase } from '@/models/caseManagement/featureCase'; import type { DetailCase } from '@/models/caseManagement/featureCase';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { Instance } from 'tippy.js';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const reviewDetail = ref<ReviewItem>({ ...reviewDefaultDetail }); const reviewDetail = ref<ReviewItem>({ ...reviewDefaultDetail });
const reviewId = ref(route.query.id as string);
const loading = ref(false); const loading = ref(false);
// //
async function initDetail() { async function initDetail() {
try { try {
loading.value = true; loading.value = true;
const res = await getReviewDetail(route.query.id as string); const res = await getReviewDetail(reviewId.value);
reviewDetail.value = res; reviewDetail.value = res;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -333,7 +331,7 @@
caseListLoading.value = true; caseListLoading.value = true;
const res = await getReviewDetailCasePage({ const res = await getReviewDetailCasePage({
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
reviewId: route.query.id as string, reviewId: reviewId.value,
viewFlag: onlyMine.value, viewFlag: onlyMine.value,
keyword: keyword.value, keyword: keyword.value,
current: pageNation.value.current, current: pageNation.value.current,
@ -470,7 +468,7 @@
async function initReviewHistoryList() { async function initReviewHistoryList() {
try { try {
reviewHistoryListLoading.value = true; reviewHistoryListLoading.value = true;
const res = await getCaseReviewHistoryList(route.query.id as string, activeCaseId.value); const res = await getCaseReviewHistoryList(reviewId.value, activeCaseId.value);
reviewHistoryList.value = res; reviewHistoryList.value = res;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -489,7 +487,6 @@
); );
const autoNext = ref(false); const autoNext = ref(false);
const dialogFormRef = ref<InstanceType<typeof reviewForm>>();
const demandKeyword = ref(''); const demandKeyword = ref('');
const caseDemandRef = ref<InstanceType<typeof caseTabDemand>>(); const caseDemandRef = ref<InstanceType<typeof caseTabDemand>>();
@ -505,51 +502,23 @@
); );
} }
const submitReviewLoading = ref(false); function reviewDone() {
// if (autoNext.value) {
function submitReview() { // id
dialogFormRef.value?.validateForm(async (caseResultForm: Record<string, any>) => { const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
try { if (index < caseList.value.length - 1) {
submitReviewLoading.value = true; activeCaseId.value = caseList.value[index + 1].caseId;
const params = { } else {
projectId: appStore.currentProjectId, //
caseId: activeCaseId.value, loadCaseDetail();
reviewId: route.query.id as string, initReviewHistoryList();
status: caseResultForm.value.result,
reviewPassRule: reviewDetail.value.reviewPassRule,
content: caseResultForm.value.reason,
notifier: '', // TODO:
};
await saveCaseReviewResult(params);
Message.success(t('caseManagement.caseReview.reviewSuccess'));
caseResultForm.value = {
result: 'PASS' as ReviewResult,
reason: '',
fileList: [] as MsFileItem[],
};
if (autoNext.value) {
// id
const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
if (index < caseList.value.length - 1) {
activeCaseId.value = caseList.value[index + 1].caseId;
} else {
//
loadCaseDetail();
initReviewHistoryList();
}
} else {
//
loadCaseDetail();
initReviewHistoryList();
}
loadCaseList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
submitReviewLoading.value = false;
} }
}); } else {
//
loadCaseDetail();
initReviewHistoryList();
}
loadCaseList();
} }
const editCaseVisible = ref(false); const editCaseVisible = ref(false);
@ -596,7 +565,7 @@
moduleIds, moduleIds,
}; };
} else { } else {
keyword.value = route.query.caseId as string; keyword.value = route.query.reviewId as string;
} }
initDetail(); initDetail();
loadCaseList(); loadCaseList();
@ -654,7 +623,12 @@
.review-history-list { .review-history-list {
@apply h-full; @apply h-full;
padding: 16px 0 16px 16px; padding: 16px 0 0 16px;
.review-history-list-item {
&:not(:last-child) {
margin-bottom: 16px;
}
}
} }
} }
.content-footer { .content-footer {

View File

@ -158,34 +158,13 @@
class="mb-0" class="mb-0"
> >
<div class="flex w-full items-center"> <div class="flex w-full items-center">
<a-mention <MsRichText
v-model:model-value="dialogForm.reason" v-model:raw="dialogForm.reason"
type="textarea" v-model:commentIds="dialogForm.commentIds"
:auto-size="{ minRows: 1 }" :upload-image="handleUploadImage"
:max-length="1000" class="w-full"
allow-clear
class="flex flex-1 items-center"
/> />
<MsUpload
v-model:file-list="dialogForm.fileList"
accept="image"
size-unit="MB"
:auto-upload="false"
multiple
:limit="10"
:disabled="dialogForm.fileList.length >= 10"
>
<a-button type="outline" class="ml-[8px] p-[8px_6px]" :disabled="dialogForm.fileList.length >= 10">
<icon-file-image :size="18" />
</a-button>
</MsUpload>
</div> </div>
<MsFileList
v-model:file-list="dialogForm.fileList"
show-mode="imageList"
:show-tab="false"
class="mt-[8px]"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
v-if="dialogShowType === 'changeReviewer'" v-if="dialogShowType === 'changeReviewer'"
@ -231,9 +210,10 @@
type="primary" type="primary"
class="ml-[12px]" class="ml-[12px]"
:loading="dialogLoading" :loading="dialogLoading"
:disabled="submitReviewDisabled"
@click="commitResult" @click="commitResult"
> >
{{ t('caseManagement.caseReview.commitResult') }} {{ t('caseManagement.caseReview.submitReview') }}
</a-button> </a-button>
<a-button <a-button
v-if="dialogShowType === 'changeReviewer'" v-if="dialogShowType === 'changeReviewer'"
@ -268,11 +248,10 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPopconfirm from '@/components/pure/ms-popconfirm/index.vue'; import MsPopconfirm from '@/components/pure/ms-popconfirm/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types'; import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
@ -284,6 +263,7 @@
getReviewDetailCasePage, getReviewDetailCasePage,
getReviewUsers, getReviewUsers,
} from '@/api/modules/case-management/caseReview'; } from '@/api/modules/case-management/caseReview';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
import { getProjectMemberCommentOptions } from '@/api/modules/project-management/projectMember'; import { getProjectMemberCommentOptions } from '@/api/modules/project-management/projectMember';
import { reviewResultMap } from '@/config/caseManagement'; import { reviewResultMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -420,14 +400,14 @@
...filter.combine, ...filter.combine,
} }
: {}, : {},
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
}; };
setLoadListParams(tableParams.value); setLoadListParams(tableParams.value);
loadList(); loadList();
emit('init', { emit('init', {
...tableParams.value, ...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [], moduleIds: [],
}); });
} }
@ -472,6 +452,7 @@
reviewer: [] as string[], reviewer: [] as string[],
isAppend: false, isAppend: false,
fileList: [] as MsFileItem[], fileList: [] as MsFileItem[],
commentIds: [] as string[],
}; };
const dialogForm = ref({ ...defaultDialogForm }); const dialogForm = ref({ ...defaultDialogForm });
const dialogFormRef = ref<FormInstance>(); const dialogFormRef = ref<FormInstance>();
@ -649,13 +630,13 @@
reviewPassRule: props.reviewPassRule, reviewPassRule: props.reviewPassRule,
status: dialogForm.value.result as ReviewResult, status: dialogForm.value.result as ReviewResult,
content: dialogForm.value.reason, content: dialogForm.value.reason,
notifier: '', // TODO: notifier: dialogForm.value.commentIds.join(';'),
selectIds: batchParams.value.selectIds, selectIds: batchParams.value.selectIds,
selectAll: batchParams.value.selectAll, selectAll: batchParams.value.selectAll,
excludeIds: batchParams.value.excludeIds, excludeIds: batchParams.value.excludeIds,
condition: batchParams.value.condition, condition: batchParams.value.condition,
}); });
Message.success(t('common.updateSuccess')); Message.success(t('caseManagement.caseReview.reviewSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
resetSelector(); resetSelector();
emit('refresh'); emit('refresh');
@ -670,6 +651,19 @@
}); });
} }
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
const submitReviewDisabled = computed(
() =>
dialogForm.value.result !== 'PASS' &&
(dialogForm.value.reason === '' || dialogForm.value.reason.trim() === '<p style=""></p>')
);
const reviewersOptions = ref<SelectOptionData[]>([]); const reviewersOptions = ref<SelectOptionData[]>([]);
const reviewerLoading = ref(false); const reviewerLoading = ref(false);

View File

@ -1,6 +1,6 @@
<template> <template>
<a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical"> <a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical">
<a-form-item field="reason" :label="t('caseManagement.caseReview.reviewResult')" class="mb-[8px]"> <a-form-item field="reason" class="mb-[4px]">
<a-radio-group v-model:model-value="caseResultForm.result" @change="() => dialogFormRef?.resetFields()"> <a-radio-group v-model:model-value="caseResultForm.result" @change="() => dialogFormRef?.resetFields()">
<a-radio value="PASS"> <a-radio value="PASS">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
@ -28,61 +28,66 @@
</a-radio> </a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item <div class="flex w-full items-center">
field="reason" <a-button type="secondary" class="p-[8px_6px]" size="small" @click="modalVisible = true">
:label="t('caseManagement.caseReview.reason')" <icon-plus class="mr-[4px]" />
:rules=" {{ t('caseManagement.caseReview.reason') }}
caseResultForm.result !== 'PASS' </a-button>
? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }] </div>
: []
"
asterisk-position="end"
class="mb-0"
>
<div class="flex w-full items-center">
<a-mention
v-model:model-value="caseResultForm.reason"
type="textarea"
:auto-size="{ minRows: 1 }"
:max-length="1000"
allow-clear
class="flex flex-1 items-center"
/>
<MsUpload
v-model:file-list="caseResultForm.fileList"
accept="image"
size-unit="MB"
:auto-upload="false"
:limit="10"
multiple
>
<a-button type="outline" class="ml-[8px] p-[8px_6px]">
<icon-file-image :size="18" />
</a-button>
</MsUpload>
</div>
<MsFileList
v-model:file-list="caseResultForm.fileList"
show-mode="imageList"
:show-tab="false"
class="mt-[8px]"
/>
</a-form-item>
</a-form> </a-form>
<a-button
type="primary"
class="mt-[12px]"
:disabled="submitDisabled"
:submit-review-loading="submitReviewLoading"
@click="submitReview"
>
{{ t('caseManagement.caseReview.submitReview') }}
</a-button>
<a-modal
v-model:visible="modalVisible"
:title="t('caseManagement.caseReview.reason')"
class="p-[4px]"
title-align="start"
body-class="p-0"
:width="680"
:cancel-button-props="{ disabled: submitReviewLoading }"
:ok-button-props="{ disabled: submitDisabled }"
:ok-loading="submitReviewLoading"
:ok-text="t('caseManagement.caseReview.submitReview')"
@before-ok="submitReview"
>
<MsRichText
v-model:raw="caseResultForm.reason"
v-model:commentIds="caseResultForm.commentIds"
:upload-image="handleUploadImage"
class="w-full"
/>
</a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsFileList from '@/components/pure/ms-upload/fileList.vue'; import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types'; import { MsFileItem } from '@/components/pure/ms-upload/types';
import { saveCaseReviewResult } from '@/api/modules/case-management/caseReview';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ReviewResult } from '@/models/caseManagement/caseReview'; import { ReviewPassRule, ReviewResult } from '@/models/caseManagement/caseReview';
const props = defineProps<{
reviewId: string;
caseId: string;
reviewPassRule: ReviewPassRule;
}>();
const emit = defineEmits(['done']);
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const dialogFormRef = ref<FormInstance>(); const dialogFormRef = ref<FormInstance>();
@ -90,23 +95,64 @@
result: 'PASS' as ReviewResult, result: 'PASS' as ReviewResult,
reason: '', reason: '',
fileList: [] as MsFileItem[], fileList: [] as MsFileItem[],
commentIds: [] as string[],
}); });
const submitReviewLoading = ref(false);
const submitDisabled = computed(
() =>
caseResultForm.value.result !== 'PASS' &&
(caseResultForm.value.reason === '' || caseResultForm.value.reason.trim() === '<p style=""></p>')
);
const modalVisible = ref(false);
function validateForm(cb: (form: Record<string, any>) => void) { async function handleUploadImage(file: File) {
dialogFormRef.value?.validate((errors) => { const { data } = await editorUploadFile({
if (!errors && typeof cb === 'function') { fileList: [file],
cb(caseResultForm.value); });
return data;
}
//
function submitReview() {
dialogFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
submitReviewLoading.value = true;
const params = {
projectId: appStore.currentProjectId,
caseId: props.caseId,
reviewId: props.reviewId,
status: caseResultForm.value.result,
reviewPassRule: props.reviewPassRule,
content: caseResultForm.value.reason,
notifier: caseResultForm.value.commentIds.join(';'),
};
await saveCaseReviewResult(params);
modalVisible.value = false;
Message.success(t('caseManagement.caseReview.reviewSuccess'));
caseResultForm.value = {
result: 'PASS' as ReviewResult,
reason: '',
fileList: [] as MsFileItem[],
commentIds: [] as string[],
};
emit('done');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
submitReviewLoading.value = false;
}
} }
}); });
} }
defineExpose({
validateForm,
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.image-preview-container { :deep(.arco-form-item-label-col) {
margin-top: 8px; margin-bottom: 0;
}
.arco-radio {
@apply pl-0;
} }
</style> </style>