feat(测试计划): 测试计划详情-功能用例详情基本信息tab和详情tab

This commit is contained in:
teukkk 2024-05-15 20:38:12 +08:00 committed by Craftsman
parent f1fe90aeb1
commit 2e08868d98
19 changed files with 539 additions and 90 deletions

View File

@ -25,6 +25,7 @@ import {
MoveTestPlanModuleUrl, MoveTestPlanModuleUrl,
planDetailBugPageUrl, planDetailBugPageUrl,
planPassRateUrl, planPassRateUrl,
RunFeatureCaseUrl,
updateTestPlanModuleUrl, updateTestPlanModuleUrl,
UpdateTestPlanUrl, UpdateTestPlanUrl,
} from '@/api/requrls/test-plan/testPlan'; } from '@/api/requrls/test-plan/testPlan';
@ -42,6 +43,7 @@ import type {
PlanDetailBugItem, PlanDetailBugItem,
PlanDetailFeatureCaseItem, PlanDetailFeatureCaseItem,
PlanDetailFeatureCaseListQueryParams, PlanDetailFeatureCaseListQueryParams,
RunFeatureCaseParams,
TestPlanDetail, TestPlanDetail,
TestPlanItem, TestPlanItem,
UseCountType, UseCountType,
@ -164,3 +166,7 @@ export function disassociateCase(data: DisassociateCaseParams) {
export function batchDisassociateCase(data: BatchFeatureCaseParams) { export function batchDisassociateCase(data: BatchFeatureCaseParams) {
return MSR.post({ url: BatchDisassociateCaseUrl, data }); return MSR.post({ url: BatchDisassociateCaseUrl, data });
} }
// 计划详情-功能用例-执行
export function runFeatureCase(data: RunFeatureCaseParams) {
return MSR.post({ url: RunFeatureCaseUrl, data });
}

View File

@ -52,3 +52,5 @@ export const GetFeatureCaseModuleUrl = '/test-plan/functional/case/tree';
export const DisassociateCaseUrl = '/test-plan/functional/case/disassociate'; export const DisassociateCaseUrl = '/test-plan/functional/case/disassociate';
// 计划详情-功能用例-批量取消关联用例 // 计划详情-功能用例-批量取消关联用例
export const BatchDisassociateCaseUrl = '/test-plan/functional/case/batch/disassociate'; export const BatchDisassociateCaseUrl = '/test-plan/functional/case/batch/disassociate';
// 计划详情-功能用例-执行
export const RunFeatureCaseUrl = '/test-plan/functional/case/run';

View File

@ -9,7 +9,7 @@
position="tr" position="tr"
trigger="click" trigger="click"
> >
<a-button :disabled="props.disabled" type="outline"> <a-button :disabled="props.disabled" type="outline" class="arco-btn-outline--secondary">
<template #icon> <icon-plus class="text-[14px]" /> </template> <template #icon> <icon-plus class="text-[14px]" /> </template>
{{ t('system.orgTemplate.addAttachment') }} {{ t('system.orgTemplate.addAttachment') }}
</a-button> </a-button>

View File

@ -1,4 +1,5 @@
import type { PassRateCountDetail, planStatusType, TestPlanDetail } from '@/models/testPlan/testPlan'; import type { PassRateCountDetail, planStatusType, TestPlanDetail } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum';
// TODO: 对照后端字段 // TODO: 对照后端字段
// 测试计划详情 // 测试计划详情
@ -39,4 +40,11 @@ export const defaultDetailCount: PassRateCountDetail = {
apiScenarioCount: 0, apiScenarioCount: 0,
}; };
export const defaultExecuteForm = {
lastExecResult: 'PASSED' as LastExecuteResults,
content: '',
planCommentFileIds: [],
notifier: [] as string[],
};
export default {}; export default {};

View File

@ -175,4 +175,9 @@ export default {
'common.moreSetting': 'More settings', 'common.moreSetting': 'More settings',
'common.remark': 'Remark', 'common.remark': 'Remark',
'common.case': 'Case', 'common.case': 'Case',
'common.caseLevel': 'Case level',
'common.caseStatus': 'Case status',
'common.responsiblePerson': 'Responsible person',
'common.updateUserName': 'Update user name',
'common.updateTime': 'Update time',
}; };

View File

@ -178,4 +178,9 @@ export default {
'common.baseInfo': '基本信息', 'common.baseInfo': '基本信息',
'common.remark': '备注', 'common.remark': '备注',
'common.case': '用例', 'common.case': '用例',
'common.caseLevel': '用例等级',
'common.caseStatus': '用例状态',
'common.responsiblePerson': '责任人',
'common.updateUserName': '更新人',
'common.updateTime': '更新时间',
}; };

View File

@ -180,4 +180,19 @@ export interface PassRateCountDetail {
apiScenarioCount: number; apiScenarioCount: number;
} }
export interface ExecuteFeatureCaseFormParams {
lastExecResult: LastExecuteResults;
content?: string;
commentIds?: string[];
planCommentFileIds?: string[];
}
export interface RunFeatureCaseParams extends ExecuteFeatureCaseFormParams {
projectId: string;
id: string;
testPlanId: string;
caseId: string;
notifier?: string;
}
export default {}; export default {};

View File

@ -3,7 +3,7 @@
<template #index="{ rowIndex }"> <template #index="{ rowIndex }">
<div class="circle text-[12px] font-medium"> {{ rowIndex + 1 }}</div> <div class="circle text-[12px] font-medium"> {{ rowIndex + 1 }}</div>
</template> </template>
<template #caseStep="{ record }"> <template v-if="!props.isTestPlan" #caseStep="{ record }">
<!-- v-if="record.showStep" --> <!-- v-if="record.showStep" -->
<a-textarea <a-textarea
:ref="(el: refItem) => setStepRefMap(el, record)" :ref="(el: refItem) => setStepRefMap(el, record)"
@ -16,7 +16,7 @@
@blur="blurHandler(record, 'step')" @blur="blurHandler(record, 'step')"
/> />
</template> </template>
<template #expectedResult="{ record }"> <template v-if="!props.isTestPlan" #expectedResult="{ record }">
<a-textarea <a-textarea
:ref="(el: refItem) => setExpectedRefMap(el, record)" :ref="(el: refItem) => setExpectedRefMap(el, record)"
v-model="record.expected" v-model="record.expected"
@ -28,6 +28,9 @@
@blur="blurHandler(record, 'expected')" @blur="blurHandler(record, 'expected')"
/> />
</template> </template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.executeResult" />
</template>
<template #operation="{ record }"> <template #operation="{ record }">
<MsTableMoreAction <MsTableMoreAction
v-if="!record.internal" v-if="!record.internal"
@ -53,6 +56,7 @@
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { getGenerateId } from '@/utils'; import { getGenerateId } from '@/utils';
@ -68,6 +72,7 @@
stepList: any; stepList: any;
isDisabled?: boolean; isDisabled?: boolean;
isScrollY?: boolean; isScrollY?: boolean;
isTestPlan?: boolean;
}>(), }>(),
{ {
isDisabled: false, isDisabled: false,
@ -100,17 +105,35 @@
{ {
title: 'system.orgTemplate.useCaseStep', title: 'system.orgTemplate.useCaseStep',
slotName: 'caseStep', slotName: 'caseStep',
dataIndex: 'caseStep', dataIndex: 'step',
showDrag: true, showDrag: true,
showInTable: true, showInTable: true,
}, },
{ {
title: 'system.orgTemplate.expectedResult', title: 'system.orgTemplate.expectedResult',
dataIndex: 'expectedResult', dataIndex: 'expected',
slotName: 'expectedResult', slotName: 'expectedResult',
showDrag: true, showDrag: true,
showInTable: true, showInTable: true,
}, },
...(!props.isTestPlan
? []
: [
{
title: 'system.orgTemplate.actualResult',
dataIndex: 'actualResult',
slotName: 'actualResult',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.stepExecutionResult',
dataIndex: 'executeResult',
slotName: 'lastExecResult',
showDrag: true,
showInTable: true,
},
]),
{ {
title: 'system.orgTemplate.operation', title: 'system.orgTemplate.operation',
slotName: 'operation', slotName: 'operation',

View File

@ -10,7 +10,7 @@
> >
<span class="absolute right-[6px] top-0"> <span class="absolute right-[6px] top-0">
<a-button <a-button
v-if="props.allowEdit" v-if="props.allowEdit && !props.isTestPlan"
v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"
type="text" type="text"
class="px-0" class="px-0"
@ -45,7 +45,7 @@
" "
class="relative" class="relative"
> >
<div class="absolute left-16 top-0 font-normal"> <div v-if="!props.isTestPlan" class="absolute left-16 top-0 font-normal">
<a-divider direction="vertical" /> <a-divider direction="vertical" />
<a-dropdown :popup-max-height="false" @select="handleSelectType"> <a-dropdown :popup-max-height="false" @select="handleSelectType">
<span class="changeType cursor-pointer text-[var(--color-text-3)]" <span class="changeType cursor-pointer text-[var(--color-text-3)]"
@ -63,7 +63,12 @@
</div> </div>
<!-- 步骤描述 --> <!-- 步骤描述 -->
<div v-if="detailForm.caseEditType === 'STEP'" class="w-full"> <div v-if="detailForm.caseEditType === 'STEP'" class="w-full">
<AddStep v-model:step-list="stepData" :is-scroll-y="false" :is-disabled="!isEditPreposition" /> <AddStep
v-model:step-list="stepData"
:is-scroll-y="false"
:is-test-plan="props.isTestPlan"
:is-disabled="!isEditPreposition"
/>
</div> </div>
<!-- 文本描述 --> <!-- 文本描述 -->
<MsRichText <MsRichText
@ -113,13 +118,13 @@
{{ t('common.save') }} {{ t('common.save') }}
</a-button></div </a-button></div
> >
<div v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"> <div v-if="!props.isTestPlan" v-permission="['FUNCTIONAL_CASE:READ+UPDATE']">
<AddAttachment v-model:file-list="fileList" multiple @change="handleChange" @link-file="associatedFile" /> <AddAttachment v-model:file-list="fileList" multiple @change="handleChange" @link-file="associatedFile" />
</div> </div>
</a-form> </a-form>
<!-- 文件列表开始 --> <!-- 文件列表开始 -->
<div class="w-[90%]"> <div class="w-[90%]">
<div v-if="!props.allowEdit" class="mb-[16px] font-medium text-[var(--color-text-1)]"> <div v-if="!props.allowEdit || props.isTestPlan" class="mb-[16px] font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.featureCase.attachment') }} {{ t('caseManagement.featureCase.attachment') }}
</div> </div>
<MsFileList <MsFileList
@ -132,7 +137,7 @@
}" }"
:upload-func="uploadOrAssociationFile" :upload-func="uploadOrAssociationFile"
:handle-delete="deleteFileHandler" :handle-delete="deleteFileHandler"
:show-delete="props.allowEdit" :show-delete="props.allowEdit && !props.isTestPlan"
@finish="uploadFileOver" @finish="uploadFileOver"
> >
<template #actions="{ item }"> <template #actions="{ item }">
@ -149,6 +154,7 @@
{{ t('ms.upload.preview') }} {{ t('ms.upload.preview') }}
</MsButton> </MsButton>
<SaveAsFilePopover <SaveAsFilePopover
v-if="!props.isTestPlan"
v-model:visible="transferVisible" v-model:visible="transferVisible"
:saving-file="activeTransferFileParams" :saving-file="activeTransferFileParams"
:file-save-as-source-id="(form.id as string)" :file-save-as-source-id="(form.id as string)"
@ -291,6 +297,7 @@
allowEdit?: boolean; // allowEdit?: boolean; //
formRules?: FormRuleItem[]; // formRules?: FormRuleItem[]; //
formApi?: any; formApi?: any;
isTestPlan?: boolean; //
}>(), }>(),
{ {
allowEdit: true, // allowEdit: true, //
@ -582,6 +589,8 @@
return { return {
step: item.desc, step: item.desc,
expected: item.result, expected: item.result,
actualResult: item.actualResult ?? '',
executeResult: item.executeResult ?? 'PASSED',
}; };
}); });
} }

View File

@ -145,7 +145,37 @@ export function getModules(moduleIds: string, treeData: ModuleTreeNode[]) {
} }
} }
// 处理自定义字段 // 自定义字段
export function getCustomField(customFields: any) {
const multipleExcludes = ['MULTIPLE_SELECT', 'CHECKBOX', 'MULTIPLE_MEMBER'];
const selectExcludes = ['MEMBER', 'RADIO', 'SELECT'];
let selectValue: Record<string, any>;
// 处理多选项
if (multipleExcludes.includes(customFields.type) && customFields.defaultValue) {
selectValue = JSON.parse(customFields.defaultValue);
return (
(customFields.options || [])
.filter((item: any) => selectValue.includes(item.value))
.map((it: any) => it.text)
.join(',') || '-'
);
}
if (customFields.type === 'MULTIPLE_INPUT') {
// 处理标签形式
return JSON.parse(customFields.defaultValue).join('') || '-';
}
if (selectExcludes.includes(customFields.type)) {
return (
(customFields.options || [])
.filter((item: any) => customFields.defaultValue === item.value)
.map((it: any) => it.text)
.join() || '-'
);
}
return customFields.defaultValue || '-';
}
// 处理表格自定义字段
export function getTableFields(customFields: CustomAttributes[], itemDataIndex: MsTableColumnData, userId: string) { export function getTableFields(customFields: CustomAttributes[], itemDataIndex: MsTableColumnData, userId: string) {
const multipleExcludes = ['MULTIPLE_SELECT', 'CHECKBOX', 'MULTIPLE_MEMBER']; const multipleExcludes = ['MULTIPLE_SELECT', 'CHECKBOX', 'MULTIPLE_MEMBER'];
const selectExcludes = ['MEMBER', 'RADIO', 'SELECT']; const selectExcludes = ['MEMBER', 'RADIO', 'SELECT'];

View File

@ -261,16 +261,7 @@
</a-spin> </a-spin>
</div> </div>
</MsCard> </MsCard>
<MsDrawer <EditCaseDetailDrawer v-model:visible="editCaseVisible" :case-id="activeCaseId" @load-case="loadCase" />
v-model:visible="editCaseVisible"
:title="t('caseManagement.caseReview.updateCase')"
:width="1200"
:ok-text="t('common.update')"
:ok-loading="updateCaseLoading"
@confirm="updateCase"
>
<caseTemplateDetail v-if="editCaseVisible" v-model:form-mode-value="editCaseForm" :case-id="activeCaseId" />
</MsDrawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -278,21 +269,19 @@
* @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';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue'; import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
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 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 caseTabDemand from '../caseManagementFeature/components/tabContent/tabDemand/associatedDemandTable.vue'; import caseTabDemand from '../caseManagementFeature/components/tabContent/tabDemand/associatedDemandTable.vue';
import caseTabDetail from '../caseManagementFeature/components/tabContent/tabDetail.vue'; import caseTabDetail from '../caseManagementFeature/components/tabContent/tabDetail.vue';
import EditCaseDetailDrawer from './components/editCaseDetailDrawer.vue';
import reviewForm from './components/reviewForm.vue'; import reviewForm from './components/reviewForm.vue';
import { import {
@ -300,7 +289,7 @@
getReviewDetail, getReviewDetail,
getReviewDetailCasePage, getReviewDetailCasePage,
} from '@/api/modules/case-management/caseReview'; } from '@/api/modules/case-management/caseReview';
import { getCaseDetail, updateCaseRequest } 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';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -309,6 +298,8 @@
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 { getCustomField } from '@/views/case-management/caseManagementFeature/components/utils';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
@ -430,34 +421,6 @@
} }
const caseDetailLoading = ref(false); const caseDetailLoading = ref(false);
function getCustomField(customFields: any) {
const multipleExcludes = ['MULTIPLE_SELECT', 'CHECKBOX', 'MULTIPLE_MEMBER'];
const selectExcludes = ['MEMBER', 'RADIO', 'SELECT'];
let selectValue: Record<string, any>;
//
if (multipleExcludes.includes(customFields.type) && customFields.defaultValue) {
selectValue = JSON.parse(customFields.defaultValue);
return (
(customFields.options || [])
.filter((item: any) => selectValue.includes(item.value))
.map((it: any) => it.text)
.join(',') || '-'
);
}
if (customFields.type === 'MULTIPLE_INPUT') {
//
return JSON.parse(customFields.defaultValue).join('') || '-';
}
if (selectExcludes.includes(customFields.type)) {
return (
(customFields.options || [])
.filter((item: any) => customFields.defaultValue === item.value)
.map((it: any) => it.text)
.join() || '-'
);
}
return customFields.defaultValue || '-';
}
// //
async function loadCaseDetail() { async function loadCaseDetail() {
try { try {
@ -588,23 +551,10 @@
} }
const editCaseVisible = ref(false); const editCaseVisible = ref(false);
const editCaseForm = ref<Record<string, any>>({});
const updateCaseLoading = ref(false);
async function updateCase() { async function loadCase() {
try { await loadCaseList();
updateCaseLoading.value = true;
await updateCaseRequest(editCaseForm.value);
editCaseVisible.value = false;
Message.success(t('caseManagement.featureCase.editSuccess'));
loadCaseList();
loadCaseDetail(); loadCaseDetail();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
updateCaseLoading.value = false;
}
} }
onBeforeMount(() => { onBeforeMount(() => {
@ -638,8 +588,7 @@
keyword.value = route.query.reviewId as string; keyword.value = route.query.reviewId as string;
} }
initDetail(); initDetail();
loadCaseList(); loadCase();
loadCaseDetail();
if (showTab.value === 'detail') { if (showTab.value === 'detail') {
initReviewHistoryList(); initReviewHistoryList();
} }

View File

@ -0,0 +1,53 @@
<template>
<MsDrawer
v-model:visible="editCaseVisible"
:title="t('caseManagement.caseReview.updateCase')"
:width="1200"
:ok-text="t('common.update')"
:ok-loading="updateCaseLoading"
@confirm="updateCase"
>
<CaseTemplateDetail v-if="editCaseVisible" v-model:form-mode-value="editCaseForm" :case-id="props.caseId" />
</MsDrawer>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import CaseTemplateDetail from '@/views/case-management/caseManagementFeature/components/caseTemplateDetail.vue';
import { updateCaseRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
caseId: string;
}>();
const emit = defineEmits<{
(e: 'loadCase'): void;
}>();
const { t } = useI18n();
const editCaseVisible = defineModel<boolean>('visible', {
required: true,
});
const editCaseForm = ref<Record<string, any>>({});
const updateCaseLoading = ref(false);
async function updateCase() {
try {
updateCaseLoading.value = true;
await updateCaseRequest(editCaseForm.value);
editCaseVisible.value = false;
Message.success(t('caseManagement.featureCase.editSuccess'));
emit('loadCase');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
updateCaseLoading.value = false;
}
}
</script>

View File

@ -89,6 +89,8 @@ export default {
'system.orgTemplate.exitPreview': 'Exit Preview', 'system.orgTemplate.exitPreview': 'Exit Preview',
'system.orgTemplate.useCaseStep': 'useCase Step', 'system.orgTemplate.useCaseStep': 'useCase Step',
'system.orgTemplate.expectedResult': 'Expected Result', 'system.orgTemplate.expectedResult': 'Expected Result',
'system.orgTemplate.actualResult': 'Actual result',
'system.orgTemplate.stepExecutionResult': 'Step execution result',
'system.orgTemplate.numberIndex': 'Index', 'system.orgTemplate.numberIndex': 'Index',
'system.orgTemplate.addStep': 'Add Step', 'system.orgTemplate.addStep': 'Add Step',
'system.orgTemplate.caseName': 'Use case name', 'system.orgTemplate.caseName': 'Use case name',

View File

@ -88,6 +88,8 @@ export default {
'system.orgTemplate.exitPreview': '退出预览', 'system.orgTemplate.exitPreview': '退出预览',
'system.orgTemplate.useCaseStep': '用例步骤', 'system.orgTemplate.useCaseStep': '用例步骤',
'system.orgTemplate.expectedResult': '预期结果', 'system.orgTemplate.expectedResult': '预期结果',
'system.orgTemplate.actualResult': '实际结果',
'system.orgTemplate.stepExecutionResult': '步骤执行结果',
'system.orgTemplate.numberIndex': '序号', 'system.orgTemplate.numberIndex': '序号',
'system.orgTemplate.addStep': '添加步骤', 'system.orgTemplate.addStep': '添加步骤',
'system.orgTemplate.caseName': '用例名称', 'system.orgTemplate.caseName': '用例名称',

View File

@ -399,7 +399,8 @@
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL_FEATURE_CASE_DETAIL, name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL_FEATURE_CASE_DETAIL,
query: { query: {
...route.query, ...route.query,
caseId: record.id, caseId: record.caseId,
testPlanCaseId: record.id,
}, },
state: { state: {
params: JSON.stringify(getTableQueryParams()), params: JSON.stringify(getTableQueryParams()),

View File

@ -0,0 +1,75 @@
<template>
<a-form ref="formRef" :model="form">
<a-form-item field="lastExecResult" class="mb-[8px]">
<a-radio-group v-model:model-value="form.lastExecResult" @change="clearContent">
<a-radio v-for="item in executionResultList" :key="item.key" :value="item.key">
<ExecuteResult :execute-result="item.key" />
</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.commentIds"
:upload-image="handleUploadImage"
:preview-url="PreviewEditorImageUrl"
class="w-full"
/>
</div>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance } from '@arco-design/web-vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { defaultExecuteForm } from '@/config/testPlan';
import type { ExecuteFeatureCaseFormParams } from '@/models/testPlan/testPlan';
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
const form = defineModel<ExecuteFeatureCaseFormParams>('form', {
required: true,
});
const formRef = ref<FormInstance>();
const executionResultList = computed(() =>
Object.values(executionResultMap).filter((item) => item.key !== 'UN_EXECUTED')
);
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
function clearContent() {
form.value = {
...defaultExecuteForm,
lastExecResult: form.value.lastExecResult,
};
}
defineExpose({
clearContent,
});
</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,91 @@
<template>
<ExecuteForm v-model:form="form" />
<!-- TODO: 双击富文本内容打开弹窗 -->
<a-button
type="primary"
class="mt-[12px]"
:disabled="submitDisabled"
:loading="submitLoading"
@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-button-props="{ disabled: submitDisabled }"
:ok-loading="submitLoading"
:ok-text="t('caseManagement.caseReview.commitResult')"
@before-ok="submit"
>
<ExecuteForm v-model:form="form" />
</a-modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import ExecuteForm from '@/views/test-plan/testPlan/detail/featureCase/components/executeForm.vue';
import { runFeatureCase } from '@/api/modules/test-plan/testPlan';
import { defaultExecuteForm } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { ExecuteFeatureCaseFormParams } from '@/models/testPlan/testPlan';
const props = defineProps<{
caseId: string;
testPlanId: string;
id: string;
}>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const form = ref<ExecuteFeatureCaseFormParams>({ ...defaultExecuteForm });
const modalVisible = ref(false);
const submitLoading = ref(false);
const submitDisabled = computed(
() =>
form.value.lastExecResult !== 'PASSED' &&
(form.value.content === '' || form.value.content?.trim() === '<p style=""></p>')
);
//
async function submit() {
try {
submitLoading.value = true;
const params = {
projectId: appStore.currentProjectId,
caseId: props.caseId,
testPlanId: props.testPlanId,
id: props.id,
lastExecResult: form.value.lastExecResult,
content: form.value.content,
notifier: form.value?.commentIds?.join(';'),
};
await runFeatureCase(params);
modalVisible.value = false;
Message.success(t('common.updateSuccess'));
form.value = { ...defaultExecuteForm };
emit('done');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
submitLoading.value = false;
}
}
</script>

View File

@ -32,7 +32,8 @@
<div <div
v-for="item of caseList" v-for="item of caseList"
:key="item.id" :key="item.id"
:class="['case-item', caseDetail.id === item.id ? 'case-item--active' : '']" :class="['case-item', caseDetail.id === item.caseId ? 'case-item--active' : '']"
@click="changeActiveCase(item)"
> >
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<div class="text-[var(--color-text-4)]">{{ item.num }}</div> <div class="text-[var(--color-text-4)]">{{ item.num }}</div>
@ -57,11 +58,13 @@
</a-spin> </a-spin>
</div> </div>
<!-- 右侧 --> <!-- 右侧 -->
<a-spin :loading="caseDetailLoading" class="relative flex flex-1 flex-col p-[16px]"> <a-spin :loading="caseDetailLoading" class="relative flex h-full flex-1 flex-col">
<div class="flex"> <div class="flex px-[16px] pt-[16px]">
<div class="mr-[24px] flex flex-1 items-center"> <div class="mr-[24px] flex flex-1 items-center">
<MsStatusTag :status="caseDetail.status || 'PREPARED'" /> <MsStatusTag :status="caseDetail.status || 'PREPARED'" />
<div class="ml-[8px] mr-[2px] font-medium text-[rgb(var(--primary-5))]">[{{ caseDetail.num }}]</div> <div class="ml-[8px] mr-[2px] cursor-pointer font-medium text-[rgb(var(--primary-5))]" @click="goCaseDetail"
>[{{ caseDetail.num }}]</div
>
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<a-tooltip :content="caseDetail.name"> <a-tooltip :content="caseDetail.name">
<div class="one-line-text max-w-[100%] font-medium"> <div class="one-line-text max-w-[100%] font-medium">
@ -70,16 +73,52 @@
</a-tooltip> </a-tooltip>
</div> </div>
</div> </div>
<a-button type="outline">{{ t('common.edit') }}</a-button> <a-button v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" type="outline" @click="editCaseVisible = true">{{
t('common.edit')
}}</a-button>
</div> </div>
<MsTab <MsTab
v-model:active-key="activeTab" v-model:active-key="activeTab"
:show-badge="false" :show-badge="false"
:content-tab-list="contentTabList" :content-tab-list="contentTabList"
no-content no-content
class="relative border-b" class="relative mx-[16px] border-b"
/> />
<div class="tab-content"> <div :class="[' flex-1', activeTab !== 'detail' ? 'tab-content' : 'overflow-hidden']">
<!-- TODO: 属性的样式 -->
<MsDescription v-if="activeTab === 'baseInfo'" :descriptions="descriptions" :column="2" />
<div v-else-if="activeTab === 'detail'" class="align-content-start flex h-full flex-col">
<CaseTabDetail is-test-plan :form="caseDetail" />
<!-- 开始执行 -->
<div class="px-[16px] py-[8px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]">
<div class="mb-[12px] flex items-center justify-between">
<div class="font-medium text-[var(--color-text-1)]">
{{ t('testPlan.featureCase.startExecution') }}
</div>
<div class="flex items-center">
<a-switch v-model:model-value="autoNext" size="small" />
<div class="mx-[8px]">{{ t('caseManagement.caseReview.autoNext') }}</div>
<a-tooltip position="right">
<template #content>
<div>{{ t('caseManagement.caseReview.autoNextTip1') }}</div>
<div>{{ t('caseManagement.caseReview.autoNextTip2') }}</div>
</template>
<icon-question-circle
class="text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-4))]"
size="16"
/>
</a-tooltip>
<!-- TODO: 缺陷 -->
</div>
</div>
<ExecuteSubmit
:id="activeId"
:case-id="activeCaseId"
:test-plan-id="route.query.id as string"
@done="executeDone"
/>
</div>
</div>
<BugList v-if="activeTab === 'defectList'" :case-id="caseDetail.id" /> <BugList v-if="activeTab === 'defectList'" :case-id="caseDetail.id" />
<!-- TODO 待写页面 还未提交 --> <!-- TODO 待写页面 还未提交 -->
<!-- <ExecutionHistory v-if="activeTab === 'executionHistory'" :case-id="caseDetail.id" /> --> <!-- <ExecutionHistory v-if="activeTab === 'executionHistory'" :case-id="caseDetail.id" /> -->
@ -87,35 +126,56 @@
</a-spin> </a-spin>
</div> </div>
</MsCard> </MsCard>
<EditCaseDetailDrawer v-model:visible="editCaseVisible" :case-id="activeCaseId" @load-case="loadCase" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import dayjs from 'dayjs';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue'; import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsPagination from '@/components/pure/ms-pagination/index'; import MsPagination from '@/components/pure/ms-pagination/index';
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import MsStatusTag from '@/components/business/ms-status-tag/index.vue'; import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import BugList from './bug/index.vue'; import BugList from './bug/index.vue';
import ExecuteSubmit from './executeSubmit.vue';
import CaseTabDetail from '@/views/case-management/caseManagementFeature/components/tabContent/tabDetail.vue';
import EditCaseDetailDrawer from '@/views/case-management/caseReview/components/editCaseDetailDrawer.vue';
import { getCaseDetail } from '@/api/modules/case-management/featureCase';
// import ExecutionHistory from '@/views/test-plan/testPlan/detail/featureCase/detail/executionHistory/index.vue'; // import ExecutionHistory from '@/views/test-plan/testPlan/detail/featureCase/detail/executionHistory/index.vue';
import { getPlanDetailFeatureCaseList } from '@/api/modules/test-plan/testPlan'; import { getPlanDetailFeatureCaseList, getTestPlanDetail } from '@/api/modules/test-plan/testPlan';
import { testPlanDefaultDetail } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { PlanDetailFeatureCaseItem } from '@/models/testPlan/testPlan'; import type { PlanDetailFeatureCaseItem, TestPlanDetail } from '@/models/testPlan/testPlan';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils'; import { executionResultMap, getCustomField } from '@/views/case-management/caseManagementFeature/components/utils';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
// TODO const planDetail = ref<TestPlanDetail>({
const planDetail = ref({ num: '111', name: '222lalallalallalalalal222lalallalallalalalal222lalallalallalalalal' }); ...testPlanDefaultDetail,
});
async function getPlanDetail() {
try {
planDetail.value = await getTestPlanDetail(route.query.id as string);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const activeCaseId = ref(route.query.caseId as string);
const activeId = ref(route.query.testPlanCaseId as string);
const keyword = ref(''); const keyword = ref('');
const lastExecResult = ref(''); const lastExecResult = ref('');
const executeResultOptions = computed(() => { const executeResultOptions = computed(() => {
@ -164,9 +224,18 @@
} }
} }
function goCaseDetail() {
window.open(
`${window.location.origin}#${
router.resolve({ name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE }).fullPath
}?id=${activeCaseId.value}&orgId=${appStore.currentOrgId}&pId=${appStore.currentProjectId}`
);
}
const caseDetail = ref<any>({}); const caseDetail = ref<any>({});
const caseDetailLoading = ref(false); const caseDetailLoading = ref(false);
const activeTab = ref('detail'); const activeTab = ref('detail');
const editCaseVisible = ref(false);
const contentTabList = ref([ const contentTabList = ref([
{ {
value: 'baseInfo', value: 'baseInfo',
@ -185,6 +254,102 @@
label: t('testPlan.featureCase.executionHistory'), label: t('testPlan.featureCase.executionHistory'),
}, },
]); ]);
const descriptions = ref<Description[]>([]);
//
async function loadCaseDetail() {
try {
caseDetailLoading.value = true;
const res = await getCaseDetail(activeCaseId.value);
caseDetail.value = res;
descriptions.value = [
{
label: t('common.belongModule'),
value: res.moduleName || t('common.root'),
},
{
label: t('common.tag'),
value: res.tags,
isTag: true,
},
{
label: t('caseManagement.featureCase.reviewResult'),
value: res.reviewStatus,
},
//
...res.customFields.map((e: Record<string, any>) => {
try {
return {
label: e.fieldName,
value: getCustomField(e),
};
} catch (error) {
return {
label: e.fieldName,
value: e.defaultValue,
};
}
}),
{
label: t('common.creator'),
value: res.createUserName,
},
{
label: t('common.createTime'),
value: dayjs(res.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
];
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
caseDetailLoading.value = false;
}
}
function changeActiveCase(item: PlanDetailFeatureCaseItem) {
if (activeCaseId.value !== item.caseId) {
activeCaseId.value = item.caseId;
activeId.value = item.id;
}
}
watch(
() => activeCaseId.value,
() => {
loadCaseDetail();
}
);
async function loadCase() {
await loadCaseList();
await loadCaseDetail();
}
const autoNext = ref(true);
async function executeDone() {
if (autoNext.value) {
// id
const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
if (index < caseList.value.length - 1) {
await loadCaseList();
activeCaseId.value = caseList.value[index + 1].caseId;
activeId.value = caseList.value[index + 1].id;
} else if (pageNation.value.current * pageNation.value.pageSize < pageNation.value.total) {
//
pageNation.value.current += 1;
await loadCaseList();
activeCaseId.value = caseList.value[0].caseId;
activeId.value = caseList.value[0].id;
} else {
//
loadCaseDetail();
loadCaseList();
}
} else {
//
loadCase();
}
}
onBeforeMount(async () => { onBeforeMount(async () => {
const lastPageParams = window.history.state.params ? JSON.parse(window.history.state.params) : null; // const lastPageParams = window.history.state.params ? JSON.parse(window.history.state.params) : null; //
@ -201,9 +366,8 @@
moduleIds, moduleIds,
}; };
} }
await loadCaseList(); getPlanDetail();
// TODO await loadCase();
caseDetail.value = caseList.value[0] ?? {};
}); });
</script> </script>
@ -231,7 +395,15 @@
} }
} }
.tab-content { .tab-content {
@apply overflow-y-auto;
padding: 16px;
.ms-scroll-bar();
}
:deep(.caseDetailWrapper) {
@apply flex-1 overflow-y-auto;
padding: 16px;
.ms-scroll-bar(); .ms-scroll-bar();
@apply py-4;
} }
</style> </style>

View File

@ -95,4 +95,5 @@ export default {
'testPlan.featureCase.disassociateTip': '确认取消关联 { name } 吗?', 'testPlan.featureCase.disassociateTip': '确认取消关联 { name } 吗?',
'testPlan.featureCase.disassociateTipContent': '取消后,影响测试计划相关统计', 'testPlan.featureCase.disassociateTipContent': '取消后,影响测试计划相关统计',
'testPlan.featureCase.batchDisassociateTipContent': '取消后,再次关联,执行结果为:未执行', 'testPlan.featureCase.batchDisassociateTipContent': '取消后,再次关联,执行结果为:未执行',
'testPlan.featureCase.startExecution': '开始执行',
}; };