feat(测试计划): 测试计划报告自定义保存&报告预览&编辑报告内容联调

This commit is contained in:
xinxin.wu 2024-07-07 10:04:25 +08:00 committed by 刘瑞斌
parent c8c096b86a
commit 432ad3f4dd
23 changed files with 371 additions and 138 deletions

View File

@ -11,7 +11,7 @@ import {
UpdateReportDetailParams,
} from '@/models/testPlan/report';
import type { ExecuteHistoryItem } from '@/models/testPlan/testPlan';
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { manualReportGenParams, PlanReportDetail } from '@/models/testPlan/testPlanReport';
// 报告列表
export function reportList(data: TableQueryParams) {
@ -160,5 +160,17 @@ export function getFunctionalExecuteStep(data: { reportId: string; shareId?: str
}
return MSR.get<ExecuteHistoryItem>({ url: `${reportUrl.ReportFunctionalStepUrl}/${data.reportId}` });
}
// 手动生成报告
export function manualReportGen(data: manualReportGenParams) {
return MSR.post({ url: reportUrl.ManualReportGenUrl, data });
}
// 获取报告布局
export function getReportLayout(reportId: string, shareId?: string) {
if (shareId) {
return MSR.get({ url: `${reportUrl.getReportShareLayoutUrl}/${shareId}/${reportId}` });
}
return MSR.get({ url: `${reportUrl.getReportLayoutUrl}/${reportId}` });
}
export default {};

View File

@ -62,3 +62,9 @@ export const ReportPlanPreviewImageUrl = '/test-plan/report/preview/md';
export const ReportFunctionalStepUrl = '/test-plan/report/detail/functional/case/step';
// 测试计划-报告-详情-功能用例明细-执行历史步骤-分享
export const ReportShareFunctionalStepUrl = '/test-plan/report/share/detail/functional/case/step';
// 测试计划-报告-详情-手动生成报告
export const ManualReportGenUrl = '/test-plan/report/manual-gen';
// 测试计划-报告-详情-获取报告布局
export const getReportLayoutUrl = '/test-plan/report/get-layout';
// 测试计划-报告-详情-获取报告布局-分享
export const getReportShareLayoutUrl = '/test-plan/report/share/get-layout';

View File

@ -41,7 +41,7 @@ export const planDetailBugPageUrl = '/test-plan/bug/page';
// 关注测试计划
export const followPlanUrl = '/test-plan/edit/follower';
// 生成报告
export const GenerateReportUrl = '/test-plan/report/gen';
export const GenerateReportUrl = '/test-plan/report/auto-gen';
// 复制测试计划
export const copyTestPlanUrl = '/test-plan/copy';
// 测试计划通过率执行进度

View File

@ -51,6 +51,7 @@ export const defaultDetailCount: PassRateCountDetail = {
},
},
nextTriggerTime: 0,
status: 'PREPARED',
};
export const defaultExecuteForm = {
@ -91,6 +92,7 @@ export const defaultReportDetail: PlanReportDetail = {
apiBugCount: 0, // 接口用例明细bug总数
scenarioBugCount: 0, // 场景用例明细bug总数
testPlanName: '',
defaultLayout: true, // 是否是默认布局
};
export const statusConfig: StatusListType[] = [

View File

@ -7,5 +7,9 @@ export enum ReportCardTypeEnum {
SUB_PLAN_DETAIL = 'SUB_PLAN_DETAIL', // 计划组子计划详情
CUSTOM_CARD = 'CUSTOM_CARD', // 自定义卡片
}
export enum FieldTypeEnum {
SYSTEM = 'SYSTEM',
RICH_TEXT = 'RICH_TEXT',
}
export default {};

View File

@ -21,8 +21,9 @@ export interface FeatureCaseItem {
export interface UpdateReportDetailParams {
id: string;
summary: string;
richTextTmpFileIds: string[];
componentId: string;
componentValue?: string;
richTextTmpFileIds?: string[];
}
export interface ApiOrScenarioCaseItem {

View File

@ -238,6 +238,7 @@ export interface PassRateCountDetail {
};
};
nextTriggerTime: number;
status: planStatusType;
}
// 执行历史

View File

@ -1,4 +1,5 @@
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { TriggerModeLabelEnum } from '@/enums/reportEnum';
import { FieldTypeEnum, ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
export interface countDetail {
success: number;
@ -31,6 +32,7 @@ export interface PlanReportDetail {
scenarioBugCount: number; // 场景用例明细bug总数
testPlanName: string;
resultStatus?: string; // 报告结果
defaultLayout: boolean; // 报告布局
}
export type detailCountKey = 'functionalCount' | 'apiCaseCount' | 'apiScenarioCount';
@ -60,8 +62,9 @@ export interface configItem {
value: ReportCardTypeEnum;
label: string;
content?: string;
system: boolean;
type: FieldTypeEnum;
enableEdit: boolean;
richTextTmpFileIds?: string[];
}
export interface customValueForm {
@ -69,3 +72,26 @@ export interface customValueForm {
label: string;
richTextTmpFileIds?: string[];
}
export interface componentItem {
name: ReportCardTypeEnum; // 组件名称
label: string; // 组件标题
type: FieldTypeEnum; // 组件分类
value?: string; // 组件内容
pos: number;
}
// 手动生成报告
export interface manualReportGenParams {
projectId: string;
testPlanId: string;
triggerMode: TriggerModeLabelEnum; // 触发方式
reportName: string;
components: componentItem[]; // 自定义组件
richTextTmpFileIds?: string[];
}
export type SelectedReportCardTypes =
| ReportCardTypeEnum.FUNCTIONAL_DETAIL
| ReportCardTypeEnum.API_CASE_DETAIL
| ReportCardTypeEnum.SCENARIO_CASE_DETAIL;

View File

@ -26,7 +26,9 @@
</a-form>
<div class="ml-[12px]">
<a-button type="secondary" @click="cancelHandler">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" @click="handleSave">{{ t('common.save') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleSave">{{
t('common.save')
}}</a-button>
</div>
</div>
</div>
@ -70,7 +72,7 @@
t(item.label)
}}</div>
<icon-close
v-if="!item.system"
v-if="item.type !== FieldTypeEnum.SYSTEM"
:style="{ 'font-size': '14px' }"
class="cursor-pointer text-[var(--color-text-3)]"
@click.stop="removeField(item)"
@ -107,7 +109,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { cloneDeep, isEqual } from 'lodash-es';
import { VueDraggable } from 'vue-draggable-plus';
@ -115,20 +117,30 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import ViewReport from '@/views/test-plan/report/detail/component/viewReport.vue';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { manualReportGen } from '@/api/modules/test-plan/report';
import { defaultReportDetail } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { getGenerateId } from '@/utils';
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import type {
configItem,
manualReportGenParams,
PlanReportDetail,
SelectedReportCardTypes,
} from '@/models/testPlan/testPlanReport';
import { TriggerModeLabelEnum } from '@/enums/reportEnum';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { FieldTypeEnum, ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { defaultCustomConfig, defaultGroupConfig, defaultSingleConfig } from './reportConfig';
import { defaultCustomConfig, defaultGroupCardConfig, defaultGroupConfig, defaultSingleConfig } from './reportConfig';
import { getSummaryDetail } from '@/views/test-plan/report/utils';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const appStore = useAppStore();
const props = defineProps<{
detailInfo: PlanReportDetail;
isDrawer?: boolean;
@ -140,11 +152,6 @@
}>();
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const showButton = ref<boolean>(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
const isError = ref(false);
const hasChange = ref(false);
@ -159,22 +166,6 @@
isError.value = false;
}
async function handleUpdateReportDetail() {
try {
await updateReportDetail({
id: detail.value.id,
summary: richText.value.summary,
richTextTmpFileIds: richText.value.richTextTmpFileIds ?? [],
});
Message.success(t('common.updateSuccess'));
showButton.value = false;
emit('updateSuccess');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
@ -182,14 +173,47 @@
}
});
const functionalCaseTotal = computed(() => getSummaryDetail(detail.value.functionalCount).caseTotal);
const apiCaseTotal = computed(() => getSummaryDetail(detail.value.apiCaseCount).caseTotal);
const scenarioCaseTotal = computed(() => getSummaryDetail(detail.value.apiScenarioCount).caseTotal);
const configList = ref<configItem[]>([]);
const cardItemList = ref<configItem[]>([]);
const detailType = [
ReportCardTypeEnum.FUNCTIONAL_DETAIL,
ReportCardTypeEnum.API_CASE_DETAIL,
ReportCardTypeEnum.SCENARIO_CASE_DETAIL,
];
const hasCaseList = computed<Record<SelectedReportCardTypes, number>>(() => {
return {
[ReportCardTypeEnum.SCENARIO_CASE_DETAIL]: scenarioCaseTotal.value,
[ReportCardTypeEnum.FUNCTIONAL_DETAIL]: functionalCaseTotal.value,
[ReportCardTypeEnum.API_CASE_DETAIL]: apiCaseTotal.value,
};
});
function filterNotHasCase(list: configItem[]) {
return list.filter((item: any) => {
if (detailType.includes(item.value)) {
return hasCaseList.value[item.value as SelectedReportCardTypes] > 0;
}
return true;
});
}
function initDefaultConfig() {
const tempDefaultGroupConfig = filterNotHasCase(defaultGroupConfig);
const tempSingleGroupConfig = filterNotHasCase(defaultSingleConfig);
configList.value = props.isGroup ? cloneDeep(tempDefaultGroupConfig) : cloneDeep(tempSingleGroupConfig);
cardItemList.value = props.isGroup ? cloneDeep(defaultGroupCardConfig) : cloneDeep(configList.value);
}
watch(
() => props.isGroup,
() => {
configList.value = props.isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
cardItemList.value = cloneDeep(configList.value);
initDefaultConfig();
},
{
immediate: true,
@ -201,17 +225,17 @@
}
function getHoverClass(cardItem: configItem) {
if (getExist(cardItem) && !cardItem.system) {
if (getExist(cardItem) && cardItem.type !== FieldTypeEnum.SYSTEM) {
return 'hover-selected-item-class';
}
if (!getExist(cardItem) && cardItem.system) {
if (!getExist(cardItem) && cardItem.type === FieldTypeEnum.SYSTEM) {
return 'hover-item-class';
}
return '';
}
function getLabelClass(cardItem: configItem) {
const isSystemColor = cardItem.system ? 'cursor-not-allowed' : '';
const isSystemColor = cardItem.type === FieldTypeEnum.SYSTEM ? 'cursor-not-allowed' : '';
return getExist(cardItem)
? `text-[var(--color-text-4)] ${isSystemColor}`
: `text-[var(--color-text-1)] cursor-pointer hover:text-[rgb(var(--primary-4))]`;
@ -238,8 +262,7 @@
//
function handleReset() {
configList.value = props.isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
cardItemList.value = cloneDeep(configList.value);
initDefaultConfig();
nextTick(() => {
hasChange.value = false;
});
@ -300,10 +323,6 @@
configList.value.splice(currentIndex, 1, currentItem);
}
const functionalCaseTotal = computed(() => getSummaryDetail(detail.value.functionalCount).caseTotal);
const apiCaseTotal = computed(() => getSummaryDetail(detail.value.apiCaseCount).caseTotal);
const scenarioCaseTotal = computed(() => getSummaryDetail(detail.value.apiScenarioCount).caseTotal);
function showItem(item: configItem) {
switch (item.value) {
case ReportCardTypeEnum.FUNCTIONAL_DETAIL:
@ -316,8 +335,61 @@
return true;
}
}
function makeParams() {
const richFileIds: string[] = [];
const addComponentList = cardItemList.value.map((item, index) => {
if (item.richTextTmpFileIds) {
richFileIds.concat(item.richTextTmpFileIds);
}
return {
// id: item.id,
name: item.value,
label: t(item.label),
type: item.type,
value: item.content || '',
pos: index + 1,
};
});
return {
projectId: appStore.currentProjectId,
testPlanId: route.query.id as string,
triggerMode: TriggerModeLabelEnum.MANUAL,
reportName: reportForm.value.reportName,
components: addComponentList,
richTextTmpFileIds: richFileIds,
};
}
const confirmLoading = ref<boolean>(false);
//
function handleSave() {}
async function handleSave() {
if (!reportForm.value.reportName) {
isError.value = true;
Message.error(t('report.detail.reportNameNotEmpty'));
return;
}
confirmLoading.value = true;
try {
const params: manualReportGenParams = makeParams();
const reportId = await manualReportGen(params);
Message.success(t('report.detail.manualGenReportSuccess'));
if (reportId) {
router.push({
name: TestPlanRouteEnum.TEST_PLAN_REPORT_DETAIL,
query: {
id: reportId,
type: props.isGroup ? 'GROUP' : 'TEST_PLAN',
},
});
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
}
</script>
<style scoped lang="less">

View File

@ -26,6 +26,13 @@
@click="handleClick"
/>
</div>
<div
v-show="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && props.canEdit"
class="mt-[16px] flex items-center gap-[12px]"
>
<a-button type="primary" @click="handleUpdateReportDetail">{{ t('common.save') }}</a-button>
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
</div>
</template>
<script setup lang="ts">
@ -52,6 +59,7 @@
const emit = defineEmits<{
(e: 'updateCustom', formValue: customValueForm): void;
(e: 'dblclick'): void;
(e: 'cancel'): void;
}>();
const innerTextForm = ref<customValueForm>({
@ -78,10 +86,23 @@
});
}
function emitDoubleClick() {
if (!props.shareId) {
emit('dblclick');
}
}
function handleUpdateReportDetail() {
emit('updateCustom', {
...innerTextForm.value,
label: innerTextForm.value.label || t('report.detail.customDefaultCardName'),
});
}
const { handleClick } = useDoubleClick(emitDoubleClick);
function handleCancel() {
emit('cancel');
}
</script>
<style scoped></style>

View File

@ -3,42 +3,55 @@ import { defaultCount } from '@/config/testPlan';
import { ApiOrScenarioCaseItem, FeatureCaseItem, ReportBugItem } from '@/models/testPlan/report';
import type { configItem } from '@/models/testPlan/testPlanReport';
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { FieldTypeEnum, ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
export const commonDefaultConfig: configItem[] = [
const summaryConfig: configItem[] = [
{
id: ReportCardTypeEnum.SUMMARY,
value: ReportCardTypeEnum.SUMMARY,
label: 'report.detail.reportSummary',
system: true,
type: FieldTypeEnum.SYSTEM,
enableEdit: false,
},
];
const subPlanConfig: configItem[] = [
{
id: ReportCardTypeEnum.SUB_PLAN_DETAIL,
value: ReportCardTypeEnum.SUB_PLAN_DETAIL,
label: 'report.detail.subPlanDetails',
type: FieldTypeEnum.SYSTEM,
enableEdit: false,
},
];
export const commonDefaultConfig: configItem[] = [
...summaryConfig,
{
id: ReportCardTypeEnum.BUG_DETAIL,
value: ReportCardTypeEnum.BUG_DETAIL,
label: 'report.detail.bugDetails',
system: true,
type: FieldTypeEnum.SYSTEM,
enableEdit: false,
},
{
id: ReportCardTypeEnum.FUNCTIONAL_DETAIL,
value: ReportCardTypeEnum.FUNCTIONAL_DETAIL,
label: 'report.detail.featureCaseDetails',
system: true,
type: FieldTypeEnum.SYSTEM,
enableEdit: false,
},
{
id: ReportCardTypeEnum.API_CASE_DETAIL,
value: ReportCardTypeEnum.API_CASE_DETAIL,
label: 'report.detail.apiCaseDetails',
system: true,
type: FieldTypeEnum.SYSTEM,
enableEdit: false,
},
{
id: ReportCardTypeEnum.SCENARIO_CASE_DETAIL,
value: ReportCardTypeEnum.SCENARIO_CASE_DETAIL,
label: 'report.detail.scenarioCaseDetails',
system: true,
type: FieldTypeEnum.SYSTEM,
enableEdit: false,
},
];
@ -47,24 +60,17 @@ export const defaultCustomConfig: configItem = {
id: '',
value: ReportCardTypeEnum.CUSTOM_CARD,
label: 'report.detail.customDefaultCardName',
system: false,
type: FieldTypeEnum.RICH_TEXT,
enableEdit: false,
content: '',
};
// 独立报告默认配置
// 独立报告默认选择列表配置
export const defaultSingleConfig: configItem[] = [...commonDefaultConfig];
// 集合报告默认配置
export const defaultGroupConfig: configItem[] = [
{
id: ReportCardTypeEnum.SUB_PLAN_DETAIL,
value: ReportCardTypeEnum.SUB_PLAN_DETAIL,
label: 'report.detail.subPlanDetails',
system: true,
enableEdit: false,
},
...commonDefaultConfig,
];
// 集合报告默认选择列表配置
export const defaultGroupConfig: configItem[] = [...subPlanConfig, ...commonDefaultConfig];
// 集合报告默认展示卡片列表配置
export const defaultGroupCardConfig: configItem[] = [...subPlanConfig, ...summaryConfig];
interface NamedItem {
name?: string;
@ -115,6 +121,7 @@ const subPlanList: PlanReportDetail = {
apiBugCount: 0,
scenarioBugCount: 0,
resultStatus: 'SUCCESS',
defaultLayout: true,
};
// 功能用例明细

View File

@ -58,9 +58,11 @@
reportId: string;
shareId?: string;
isPreview?: boolean;
isGroup?: boolean;
}>();
const { t } = useI18n();
const columns: MsTableColumn = [
const staticColumns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
@ -97,6 +99,8 @@
},
width: 150,
},
];
const lastStaticColumns: MsTableColumn = [
{
title: 'common.belongModule',
dataIndex: 'moduleName',
@ -123,12 +127,29 @@
width: 100,
},
];
// TODO
const testPlanNameColumns: MsTableColumn = [
{
title: 'report.plan.name',
dataIndex: 'name',
showTooltip: true,
width: 200,
},
];
const columns = computed(() => {
if (props.isGroup) {
return [...staticColumns, ...testPlanNameColumns, ...lastStaticColumns];
}
return [...staticColumns, ...lastStaticColumns];
});
const reportFeatureCaseList = () => {
return !props.shareId ? getReportFeatureCaseList : getReportShareFeatureCaseList;
};
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportFeatureCaseList(), {
scroll: { x: '100%' },
columns,
columns: columns.value,
heightUsed: 20,
showSelectorAll: false,
});

View File

@ -1,7 +1,7 @@
<template>
<div :class="`${hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId ? '' : 'cursor-not-allowed'}`">
<MsRichText
v-model:raw="innerSummary.summary"
v-model:raw="innerSummary.content"
v-model:filedIds="innerSummary.richTextTmpFileIds"
:upload-image="handleUploadImage"
:preview-url="ReportPlanPreviewImageUrl"
@ -45,7 +45,7 @@
const { t } = useI18n();
const props = defineProps<{
richText: { summary: string; richTextTmpFileIds?: string[] };
richText: { content: string; label: string; richTextTmpFileIds?: string[] };
shareId?: string;
showButton: boolean;
isPlanGroup: boolean;
@ -128,8 +128,10 @@
}
function emitDoubleClick() {
if (!props.shareId) {
emit('dblclick');
}
}
const { handleClick } = useDoubleClick(emitDoubleClick);
</script>

View File

@ -157,7 +157,7 @@
</a-tooltip>
<a-divider v-if="allowEdit(item.value)" direction="vertical" class="!m-0 !mx-2" />
<a-tooltip :content="t('common.delete')">
<MsIcon type="icon-icon_delete-trash_outlined" size="16" @click="deleteCard(item)" />
<MsIcon type="icon-icon_delete-trash_filled" size="16" @click="deleteCard(item)" />
</a-tooltip>
</div>
</div>
@ -174,15 +174,19 @@
/>
<Summary
v-else-if="item.value === ReportCardTypeEnum.SUMMARY"
v-model:richText="richText"
:rich-text="{
content: item.content || '',
label: t(item.label),
richTextTmpFileIds: [],
}"
:share-id="shareId"
:can-edit="item.enableEdit"
:show-button="showButton"
:is-plan-group="props.isGroup"
:detail="detail"
@update-summary="handleUpdateReportDetail"
@cancel="() => handleCancel(item)"
@handle-summary="handleSummary"
@update-summary="handleUpdateReportDetail(item)"
@cancel="() => handleCancelCustom(item)"
@handle-summary="(value:string) => handleSummary(value,item)"
@dblclick="handleDoubleClick(item)"
/>
<BugTable
@ -197,6 +201,7 @@
:report-id="detail.id"
:share-id="shareId"
:is-preview="props.isPreview"
:is-group="props.isGroup"
/>
<ApiAndScenarioTable
v-else-if="
@ -220,6 +225,7 @@
}"
@update-custom="(formValue:customValueForm)=>updateCustom(formValue,item)"
@dblclick="handleDoubleClick(item)"
@cancel="() => handleCancelCustom(item)"
/>
</MsCard>
</div>
@ -249,11 +255,12 @@
import Summary from '@/views/test-plan/report/detail/component/system-card/summary.vue';
import SystemTrigger from '@/views/test-plan/report/detail/component/system-card/systemTrigger.vue';
import { updateReportDetail } from '@/api/modules/test-plan/report';
import { getReportLayout, updateReportDetail } from '@/api/modules/test-plan/report';
import { commonConfig, defaultCount, defaultReportDetail, seriesConfig, statusConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import { UpdateReportDetailParams } from '@/models/testPlan/report';
import type {
configItem,
countDetail,
@ -264,6 +271,7 @@
import { customValueForm } from '@/models/testPlan/testPlanReport';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { defaultGroupConfig, defaultSingleConfig } from './reportConfig';
import { getSummaryDetail } from '@/views/test-plan/report/utils';
const { t } = useI18n();
@ -277,7 +285,6 @@
}>();
const emit = defineEmits<{
(e: 'updateSuccess'): void;
(e: 'updateSuccess'): void;
(e: 'updateCustom', item: configItem): void;
}>();
@ -293,18 +300,10 @@
summary: '',
});
const isError = ref(false);
const reportForm = ref({
reportName: '',
});
function inputHandler(value: string) {
if (value.trim().length === 0) {
isError.value = true;
}
isError.value = false;
}
const getAnalysisHover = computed(() => (props.isPreview ? '' : 'hover-analysis cursor-not-allowed'));
/**
@ -388,22 +387,6 @@
scenarioCaseOptions.value.series.data = getPassRateData(apiScenarioCount);
}
async function handleUpdateReportDetail() {
try {
await updateReportDetail({
id: detail.value.id,
summary: richText.value.summary,
richTextTmpFileIds: richText.value.richTextTmpFileIds ?? [],
});
Message.success(t('common.updateSuccess'));
showButton.value = false;
emit('updateSuccess');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{
name: t('report.detail.threshold'),
@ -473,12 +456,42 @@
return count;
});
const originLayoutInfo = ref([]);
async function getDefaultLayout() {
try {
const res = await getReportLayout(detail.value.id, shareId.value);
const result = res.map((item: any) => {
return {
id: item.id,
value: item.name,
label: item.label,
content: item.value || '',
type: item.type,
enableEdit: false,
richTextTmpFileIds: item.richTextTmpFileIds,
};
});
innerCardList.value = result;
originLayoutInfo.value = cloneDeep(result);
} catch (error) {
console.log(error);
}
}
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
richText.value.summary = detail.value.summary;
reportForm.value.reportName = detail.value.name;
initOptionsData();
if (props.isPreview) {
if (!detail.value.defaultLayout && detail.value.id) {
getDefaultLayout();
} else {
innerCardList.value = props.isGroup ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
}
}
}
});
@ -491,14 +504,18 @@
});
});
function handleCancel(cardItem: configItem) {
richText.value = { summary: detail.value.summary };
function handleCancelCustom(cardItem: configItem) {
const originItem = originLayoutInfo.value.find((item: configItem) => item.id === cardItem.id);
const index = originLayoutInfo.value.findIndex((e: configItem) => e.id === cardItem.id);
if (originItem && index !== -1) {
innerCardList.value.splice(index, 1, originItem);
}
showButton.value = false;
cardItem.enableEdit = false;
}
function handleSummary(content: string) {
richText.value.summary = content;
function handleSummary(content: string, cardItem: configItem) {
cardItem.content = content;
}
const currentMode = ref<string>('drawer');
@ -512,7 +529,7 @@
}
}
const allowEditType = [ReportCardTypeEnum.SUMMARY, ReportCardTypeEnum.CUSTOM_CARD];
const allowEditType = [ReportCardTypeEnum.CUSTOM_CARD];
function allowEdit(value: ReportCardTypeEnum) {
return allowEditType.includes(value);
}
@ -537,26 +554,51 @@
const deleteCard = (cardItem: configItem) => {
innerCardList.value = innerCardList.value.filter((item) => item.id !== cardItem.id);
};
//
function editField(cardItem: configItem) {
if (allowEditType.includes(cardItem.value)) {
cardItem.enableEdit = !cardItem.enableEdit;
}
if (cardItem.value === ReportCardTypeEnum.SUMMARY) {
showButton.value = true;
}
}
function handleDoubleClick(cardItem: configItem) {
editField(cardItem);
if (props.isPreview) {
if (cardItem.value === ReportCardTypeEnum.SUMMARY) {
showButton.value = true;
}
cardItem.enableEdit = !cardItem.enableEdit;
}
}
async function handleUpdateReportDetail(currentItem: configItem) {
try {
const params: UpdateReportDetailParams = {
id: detail.value.id,
componentId: currentItem.type,
componentValue: currentItem.content,
richTextTmpFileIds: currentItem.richTextTmpFileIds,
};
await updateReportDetail(params);
Message.success(t('common.updateSuccess'));
if (currentItem.value === ReportCardTypeEnum.SUMMARY) {
showButton.value = false;
} else {
currentItem.enableEdit = !currentItem.enableEdit;
}
// TODO
emit('updateSuccess');
} catch (error) {
console.log(error);
}
}
function updateCustom(formValue: customValueForm, currentItem: configItem) {
const newCurrentItem = {
const newCurrentItem: configItem = {
...currentItem,
...formValue,
};
innerCardList.value = innerCardList.value.map((item) => {
innerCardList.value = innerCardList.value.map((item: configItem) => {
if (item.id === currentItem.id) {
return {
...item,
@ -566,7 +608,11 @@
}
return item;
});
if (!props.isPreview) {
emit('updateCustom', newCurrentItem);
} else {
handleUpdateReportDetail(newCurrentItem);
}
}
</script>

View File

@ -61,6 +61,7 @@
apiBugCount: 0,
scenarioBugCount: 0,
testPlanName: '',
defaultLayout: true,
});
const isGroup = computed(() => route.query.type === 'GROUP');

View File

@ -1,9 +1,14 @@
<template>
<ViewReport v-model:card-list="cardItemList" :detail-info="detail" :is-group="isGroup" is-preview />
<ViewReport
v-model:card-list="cardItemList"
:detail-info="detail"
:is-group="isGroup"
is-preview
@update-success="getDetail()"
/>
</template>
<script setup lang="ts">
// TODO
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { cloneDeep } from 'lodash-es';
@ -15,8 +20,6 @@
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { defaultGroupConfig, defaultSingleConfig } from '@/views/test-plan/report/detail/component/reportConfig';
const route = useRoute();
const reportId = ref<string>(route.query.id as string);
@ -27,7 +30,6 @@
const cardItemList = ref<configItem[]>([]);
async function getDetail() {
cardItemList.value = isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
try {
detail.value = await getReportDetail(reportId.value);
} catch (error) {

View File

@ -3,7 +3,6 @@
</template>
<script setup lang="ts">
// TODO
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { cloneDeep } from 'lodash-es';
@ -16,8 +15,6 @@
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { defaultGroupConfig, defaultSingleConfig } from '@/views/test-plan/report/detail/component/reportConfig';
const route = useRoute();
const router = useRouter();
const reportId = ref<string>(route.query.id as string);
@ -26,7 +23,6 @@
const cardItemList = ref<configItem[]>([]);
async function getShareDetail() {
cardItemList.value = isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
try {
const hrefShareDetail = await planGetShareHref(route.query.shareId as string);
reportId.value = hrefShareDetail.reportId;
@ -51,6 +47,7 @@
}
detail.value = await getReportDetail(reportId.value, route.query.shareId as string);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}

View File

@ -55,4 +55,6 @@ export default {
'report.detail.customMaxNumber': 'A maximum of 10 can be added',
'report.detail.enterReportNamePlaceHolder': 'Please enter a report name',
'report.detail.systemInternalTooltip': 'System built-in, not editable',
'report.detail.reportNameNotEmpty': 'The report name cannot be empty',
'report.detail.manualGenReportSuccess': 'The custom generated report was successful',
};

View File

@ -55,4 +55,6 @@ export default {
'report.detail.customMaxNumber': '最多可添加10个',
'report.detail.enterReportNamePlaceHolder': '请输入报告名称',
'report.detail.systemInternalTooltip': '系统内置,不可编辑',
'report.detail.reportNameNotEmpty': '报告名称不能为空',
'report.detail.manualGenReportSuccess': '自定义生成报告成功',
};

View File

@ -11,7 +11,7 @@
/>
<span :class="getIconClass">{{ record.childrenCount || (record.children || []).length || 0 }}</span>
</div>
<div v-if="record.type === testPlanTypeEnum.TEST_PLAN || !record.integrated" :class="`one-line-text ${hasIndent}`">
<div v-if="showButton" :class="`one-line-text ${hasIndent}`">
<MsButton type="text" @click="handleAction">
<a-tooltip :content="content">
<span>{{ record[props.numKey || 'num'] }}</span>
@ -62,7 +62,7 @@
const hasIndent = computed(() =>
(props.record.type === testPlanTypeEnum.TEST_PLAN && props.record.groupId && props.record.groupId !== 'NONE') ||
(!props.record.integrated && props.record.parentId)
(!props.record.integrated && props.record.parent)
? 'pl-[36px]'
: ''
);
@ -76,6 +76,13 @@
emit('action');
}
}
const showButton = computed(() => {
if (props.record.type) {
return props.record.type === testPlanTypeEnum.TEST_PLAN;
}
return !props.record.integrated;
});
</script>
<style scoped lang="less"></style>

View File

@ -129,7 +129,8 @@
<MsStatusTag :status="filterContent.value" />
</template>
<template #status="{ record }">
<MsStatusTag :status="record.status" />
<MsStatusTag v-if="getStatus(record.id)" :status="getStatus(record.id)" />
<span v-else>-</span>
</template>
<template #createUser="{ record }">
<a-tooltip :content="`${record.createUserName}`" position="tl">
@ -244,13 +245,13 @@
></a-divider>
<MsButton
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && record.status !== 'ARCHIVED'"
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && getStatus(record.id) !== 'ARCHIVED'"
class="!mx-0"
@click="emit('edit', record)"
>{{ t('common.edit') }}</MsButton
>
<a-divider
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && record.status !== 'ARCHIVED'"
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && getStatus(record.id) !== 'ARCHIVED'"
direction="vertical"
:margin="8"
></a-divider>
@ -259,7 +260,7 @@
v-if="
!isShowExecuteButton(record) &&
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
record.status !== 'ARCHIVED'
getStatus(record.id) !== 'ARCHIVED'
"
class="!mx-0"
@click="copyTestPlanOrGroup(record.id)"
@ -269,7 +270,7 @@
v-if="
!isShowExecuteButton(record) &&
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
record.status !== 'ARCHIVED'
getStatus(record.id) !== 'ARCHIVED'
"
direction="vertical"
:margin="8"
@ -738,17 +739,20 @@
function getScheduleEnable(id: string) {
return defaultCountDetailMap.value[id].scheduleConfig.enable;
}
function getStatus(id: string) {
return defaultCountDetailMap.value[id]?.status;
}
function isShowExecuteButton(record: TestPlanItem) {
return (
((record.type === testPlanTypeEnum.TEST_PLAN && getFunctionalCount(record.id) > 0) ||
(record.type === testPlanTypeEnum.GROUP && record.childrenCount)) &&
record.status !== 'ARCHIVED'
getStatus(record.id) !== 'ARCHIVED'
);
}
function getMoreActions(record: TestPlanItem) {
const { status: planStatus } = record;
const planStatus = getStatus(record.id);
//
const copyAction =

View File

@ -10,7 +10,7 @@
hide-back
>
<template #headerLeft>
<MsStatusTag :status="detail.status || 'PREPARED'" />
<MsStatusTag :status="countDetail.status" />
<a-tooltip :content="`[${detail.num}]${detail.name}`">
<div class="one-line-text ml-[8px] max-w-[360px] gap-[4px] font-medium text-[var(--color-text-1)]">
<span>[{{ detail.num }}]</span>
@ -33,7 +33,6 @@
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }}
</MsButton>
<!-- TODO 等待联调 接口需要调整和增加 -->
<MsTableMoreAction :list="reportMoreAction" @select="handleMoreReportSelect">
<MsButton
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) && detail.status !== 'ARCHIVED'"

View File

@ -30,8 +30,6 @@
import type { configItem, PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { defaultSingleConfig } from '@/views/test-plan/report/detail/component/reportConfig';
const props = defineProps<{
reportId: string;
}>();
@ -42,7 +40,7 @@
const route = useRoute();
const cardItemList = ref<configItem[]>(cloneDeep(defaultSingleConfig));
const cardItemList = ref<configItem[]>([]);
const shareId = ref<string>(route.query.shareId as string);