mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-12-03 12:39:12 +08:00
feat(测试计划): 测试计划报告完善和报告分享接口联调
This commit is contained in:
parent
06fd44ce12
commit
2983288864
@ -1,6 +1,7 @@
|
|||||||
import MSR from '@/api/http';
|
import MSR from '@/api/http';
|
||||||
import * as reportUrl from '@/api/requrls/test-plan/report';
|
import * as reportUrl from '@/api/requrls/test-plan/report';
|
||||||
|
|
||||||
|
import type { GetShareId } from '@/models/apiTest/report';
|
||||||
import { CommonList, TableQueryParams } from '@/models/common';
|
import { CommonList, TableQueryParams } from '@/models/common';
|
||||||
import { FeatureCaseItem, ReportBugItem, UpdateReportDetailParams } from '@/models/testPlan/report';
|
import { FeatureCaseItem, ReportBugItem, UpdateReportDetailParams } from '@/models/testPlan/report';
|
||||||
|
|
||||||
@ -59,4 +60,21 @@ export function getReportDetail(id: string) {
|
|||||||
return MSR.get({ url: `${reportUrl.PlanReportDetailUrl}/${id}` });
|
return MSR.get({ url: `${reportUrl.PlanReportDetailUrl}/${id}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试计划-报告-详情-分享
|
||||||
|
export function planReportShare(data: GetShareId) {
|
||||||
|
return MSR.post({ url: reportUrl.PlanReportShareUrl, data });
|
||||||
|
}
|
||||||
|
// 测试计划-报告-分享详情查看
|
||||||
|
export function planReportShareDetail(shareId: string, reportId: string) {
|
||||||
|
return MSR.get({ url: `${reportUrl.PlanReportShareDetailUrl}/${shareId}/${reportId}` });
|
||||||
|
}
|
||||||
|
// 测试计划-报告-获取分享链接
|
||||||
|
export function planGetShareHref(id: string) {
|
||||||
|
return MSR.get({ url: `${reportUrl.PlanGetShareHrefDetailUrl}/${id}` });
|
||||||
|
}
|
||||||
|
// 测试计划-报告-获取分享链接时效
|
||||||
|
export function getShareValidity(id: string) {
|
||||||
|
return MSR.get({ url: `${reportUrl.GetShareValidityUrl}/${id}` });
|
||||||
|
}
|
||||||
|
|
||||||
export default {};
|
export default {};
|
||||||
|
@ -18,5 +18,13 @@ export const ReportFeatureCaseListUrl = '/test-plan/report/detail/functional/cas
|
|||||||
export const ReportShareFeatureCaseListUrl = '/test-plan/report/share/detail/functional/case/page';
|
export const ReportShareFeatureCaseListUrl = '/test-plan/report/share/detail/functional/case/page';
|
||||||
// 测试计划-报告-详情-报告内容更新
|
// 测试计划-报告-详情-报告内容更新
|
||||||
export const UpdateReportDetailUrl = '/test-plan/report/detail/edit';
|
export const UpdateReportDetailUrl = '/test-plan/report/detail/edit';
|
||||||
|
// 测试计划-报告-详情-分享
|
||||||
|
export const PlanReportShareUrl = '/test-plan/report/share/gen';
|
||||||
|
// 测试计划-报告-分享详情查看
|
||||||
|
export const PlanReportShareDetailUrl = '/test-plan/report/share/get/detail';
|
||||||
|
// 测试计划-报告-获取分享链接
|
||||||
|
export const PlanGetShareHrefDetailUrl = '/test-plan/report/share/get';
|
||||||
|
// 测试计划-报告-获取分享链接时效
|
||||||
|
export const GetShareValidityUrl = '/test-plan/report/share/get-share-time';
|
||||||
// 测试计划-报告-详情-富文本编辑器上传图片文件
|
// 测试计划-报告-详情-富文本编辑器上传图片文件
|
||||||
export const EditorUploadFileUrl = '/test-plan/report/upload/md/file';
|
export const EditorUploadFileUrl = '/test-plan/report/upload/md/file';
|
||||||
|
@ -108,6 +108,7 @@ export enum ShareEnum {
|
|||||||
SHARE = 'share',
|
SHARE = 'share',
|
||||||
SHARE_REPORT_SCENARIO = 'shareReportScenario',
|
SHARE_REPORT_SCENARIO = 'shareReportScenario',
|
||||||
SHARE_REPORT_CASE = 'shareReportCase',
|
SHARE_REPORT_CASE = 'shareReportCase',
|
||||||
|
SHARE_REPORT_TEST_PLAN = 'shareReportTestPlan',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RouteEnum = {
|
export const RouteEnum = {
|
||||||
|
@ -1,25 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view v-slot="{ Component, route }">
|
<a-layout class="layout-content">
|
||||||
<transition name="fade" mode="out-in" appear>
|
<router-view v-slot="{ Component, route }">
|
||||||
<!-- transition内必须有且只有一个根元素,不然会导致二级路由的组件无法渲染 -->
|
<transition name="fade" mode="out-in" appear>
|
||||||
<div v-show="true" class="page-content">
|
<!-- transition内必须有且只有一个根元素,不然会导致二级路由的组件无法渲染 -->
|
||||||
<!-- TODO 实验性组件,以后优化 -->
|
<div v-show="true" class="page-content">
|
||||||
<keep-alive>
|
<!-- TODO 实验性组件,以后优化 -->
|
||||||
<component :is="Component" :key="route.fullPath" />
|
<keep-alive>
|
||||||
</keep-alive>
|
<Suspense> <component :is="Component" :key="route.fullPath" /> </Suspense>
|
||||||
</div>
|
</keep-alive>
|
||||||
</transition>
|
</div>
|
||||||
</router-view>
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</a-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup></script>
|
||||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.page-content {
|
.layout-content {
|
||||||
@apply h-full overflow-y-auto;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
@apply box-content overflow-y-hidden;
|
||||||
|
|
||||||
min-height: 500px;
|
height: 100vh;
|
||||||
|
background-color: var(--color-bg-3);
|
||||||
|
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
|
.arco-layout-content {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 16px 16px 0;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
min-height: 500px;
|
||||||
|
background: var(--color-text-n9);
|
||||||
|
@apply h-full w-full overflow-y-auto p-4;
|
||||||
|
.ms-scroll-bar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.arco-scrollbar-container) {
|
||||||
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -28,6 +28,11 @@ export const WHITE_LIST = [
|
|||||||
path: '/shareReportCase',
|
path: '/shareReportCase',
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'shareReportTestPlan',
|
||||||
|
path: '/shareReportTestPlan',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 左侧菜单底部对齐的菜单数组,数组项为一级路由的name
|
// 左侧菜单底部对齐的菜单数组,数组项为一级路由的name
|
||||||
|
@ -13,7 +13,7 @@ const ShareRoute: AppRouteRecordRaw = {
|
|||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
// 测试计划
|
// 接口测试-场景报告-详情
|
||||||
{
|
{
|
||||||
path: 'shareReportScenario',
|
path: 'shareReportScenario',
|
||||||
name: ShareEnum.SHARE_REPORT_SCENARIO,
|
name: ShareEnum.SHARE_REPORT_SCENARIO,
|
||||||
@ -24,6 +24,7 @@ const ShareRoute: AppRouteRecordRaw = {
|
|||||||
isTopMenu: false,
|
isTopMenu: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 接口测试-用例报告-详情
|
||||||
{
|
{
|
||||||
path: 'shareReportCase',
|
path: 'shareReportCase',
|
||||||
name: ShareEnum.SHARE_REPORT_CASE,
|
name: ShareEnum.SHARE_REPORT_CASE,
|
||||||
@ -34,6 +35,17 @@ const ShareRoute: AppRouteRecordRaw = {
|
|||||||
isTopMenu: false,
|
isTopMenu: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 测试计划-报告-详情
|
||||||
|
{
|
||||||
|
path: 'shareReportTestPlan',
|
||||||
|
name: ShareEnum.SHARE_REPORT_TEST_PLAN,
|
||||||
|
component: () => import('@/views/test-plan/report/detail/sharePlanReportIndex.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: '',
|
||||||
|
roles: ['*'],
|
||||||
|
isTopMenu: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -398,7 +398,6 @@
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.report-container {
|
.report-container {
|
||||||
padding: 16px;
|
|
||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
background: var(--color-text-n9);
|
background: var(--color-text-n9);
|
||||||
.report-header {
|
.report-header {
|
||||||
|
@ -375,7 +375,6 @@
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.report-container {
|
.report-container {
|
||||||
padding: 16px;
|
|
||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
background: var(--color-text-n9);
|
background: var(--color-text-n9);
|
||||||
.report-header {
|
.report-header {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<MsBaseTable
|
<MsBaseTable
|
||||||
v-bind="propsRes"
|
v-bind="propsRes"
|
||||||
:action-config="tableBatchActions"
|
:action-config="tableBatchActions"
|
||||||
:selectable="hasAnyPermission(['ORGANIZATION_MEMBER:READ+UPDATE'])"
|
:selectable="hasAnyPermission(['ORGANIZATION_MEMBER:READ+UPDATE', 'PROJECT_USER:READ+DELETE'])"
|
||||||
@selected-change="handleTableSelect"
|
@selected-change="handleTableSelect"
|
||||||
v-on="propsEvent"
|
v-on="propsEvent"
|
||||||
@batch-action="handleTableBatch"
|
@batch-action="handleTableBatch"
|
||||||
|
@ -0,0 +1,500 @@
|
|||||||
|
<template>
|
||||||
|
<MsCard class="mb-[16px]" hide-back hide-footer auto-height no-content-padding hide-divider>
|
||||||
|
<template #headerLeft>
|
||||||
|
<div class="flex items-center font-medium"
|
||||||
|
>{{ t('report.name') }}
|
||||||
|
<a-tooltip :content="detail.name" :mouse-enter-delay="300"
|
||||||
|
><div class="one-line-text max-w-[300px]">{{ detail.name }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #headerRight>
|
||||||
|
<a-popover position="bottom" content-class="response-popover-content">
|
||||||
|
<div>
|
||||||
|
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
|
||||||
|
{{ detail.executeTime ? dayjs(detail.executeTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
|
||||||
|
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
|
||||||
|
{{ detail.endTime ? dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="max-w-[400px] items-center gap-[8px] text-[14px]">
|
||||||
|
<div class="flex-shrink-0 text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
{{ dayjs(detail.executeTime).format('YYYY-MM-DD HH:mm:ss') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
<MsButton
|
||||||
|
v-if="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+SHARE']) && !shareId"
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="ml-4 !rounded-[var(--border-radius-small)]"
|
||||||
|
:loading="shareLoading"
|
||||||
|
@click="shareHandler"
|
||||||
|
>
|
||||||
|
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
||||||
|
{{ t('common.share') }}
|
||||||
|
</MsButton>
|
||||||
|
</template>
|
||||||
|
</MsCard>
|
||||||
|
<div class="analysis-wrapper">
|
||||||
|
<div class="analysis">
|
||||||
|
<div>
|
||||||
|
<div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div>
|
||||||
|
<ul class="report-analysis">
|
||||||
|
<li class="report-analysis-item">
|
||||||
|
<div class="report-analysis-item-icon">
|
||||||
|
<svg-icon class="mr-2" width="24px" height="24px" name="threshold" />
|
||||||
|
<span>{{ t('report.detail.threshold') }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="report-analysis-item-number">{{ detail.passThreshold }}</span>
|
||||||
|
<span class="report-analysis-item-unit">(%)</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="report-analysis-item">
|
||||||
|
<div class="report-analysis-item-icon">
|
||||||
|
<svg-icon class="mr-2" width="24px" height="24px" name="passRate" />
|
||||||
|
<span>{{ t('report.detail.reportPassRate') }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="report-analysis-item-number">{{ detail.passRate }}</span>
|
||||||
|
<span class="report-analysis-item-unit">(%)</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="report-analysis-item">
|
||||||
|
<div class="report-analysis-item-icon">
|
||||||
|
<svg-icon class="mr-2" width="24px" height="24px" name="passRate" />
|
||||||
|
<span>{{ t('report.detail.performCompletion') }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="report-analysis-item-number">{{ detail.executeRate }}</span>
|
||||||
|
<span class="report-analysis-item-unit">(%)</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="report-analysis-item">
|
||||||
|
<div class="report-analysis-item-icon">
|
||||||
|
<svg-icon class="mr-2" width="24px" height="24px" name="bugTotal" />
|
||||||
|
<span>{{ t('report.detail.totalDefects') }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="report-analysis-item-number">{{ addCommasToNumber(detail.bugCount) }}</span>
|
||||||
|
<span class="report-analysis-item-unit">({{ t('report.detail.number') }})</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis mx-4">
|
||||||
|
<div>
|
||||||
|
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
|
||||||
|
<SetReportChart
|
||||||
|
size="160px"
|
||||||
|
offset="top-[34%] right-0 bottom-0 left-0"
|
||||||
|
:legend-data="legendData"
|
||||||
|
:options="charOptions"
|
||||||
|
:request-total="getIndicators(detail.caseTotal) || 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis">
|
||||||
|
<div>
|
||||||
|
<div class="block-title">{{ t('report.detail.useCaseAnalysis') }}</div>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="w-[70%]">
|
||||||
|
<SingleStatusProgress :detail="detail" status="pending" />
|
||||||
|
<SingleStatusProgress :detail="detail" status="success" />
|
||||||
|
<SingleStatusProgress :detail="detail" status="block" />
|
||||||
|
<SingleStatusProgress :detail="detail" status="error" />
|
||||||
|
</div>
|
||||||
|
<div class="relative w-[30%]">
|
||||||
|
<div class="charts absolute w-full text-center">
|
||||||
|
<div class="text-[12px] !text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
|
||||||
|
<a-popover position="bottom" content-class="response-popover-content">
|
||||||
|
<div class="flex justify-center text-[18px] font-medium">
|
||||||
|
<div class="one-line-text max-w-[60px] text-[var(--color-text-1)]">{{ detail.passRate }}% </div>
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="min-w-[95px] max-w-[400px] p-4 text-[14px]">
|
||||||
|
<div class="text-[12px] font-medium text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
|
||||||
|
<div class="mt-2 text-[18px] font-medium text-[var(--color-text-1)]">{{ detail.passRate }} %</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
<div class="flex h-full w-full items-center justify-center">
|
||||||
|
<MsChart width="120px" height="120px" :options="functionCaseOptions"
|
||||||
|
/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MsCard class="mb-[16px]" simple auto-height>
|
||||||
|
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
|
||||||
|
<MsRichText
|
||||||
|
v-model:raw="richText.summary"
|
||||||
|
v-model:filedIds="richText.richTextTmpFileIds"
|
||||||
|
:upload-image="handleUploadImage"
|
||||||
|
:preview-url="PreviewEditorImageUrl"
|
||||||
|
class="mt-[8px] w-full"
|
||||||
|
/>
|
||||||
|
<div v-show="showButton" 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>
|
||||||
|
</MsCard>
|
||||||
|
<MsCard simple auto-height>
|
||||||
|
<MsTab
|
||||||
|
v-model:active-key="activeTab"
|
||||||
|
:show-badge="false"
|
||||||
|
:content-tab-list="contentTabList"
|
||||||
|
no-content
|
||||||
|
class="relative mb-[16px] border-b"
|
||||||
|
/>
|
||||||
|
<BugTable v-if="activeTab === 'bug'" :report-id="reportId" :share-id="shareId" />
|
||||||
|
<FeatureCaseTable v-if="activeTab === 'featureCase'" :report-id="reportId" :share-id="shareId" />
|
||||||
|
</MsCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import MsChart from '@/components/pure/chart/index.vue';
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||||
|
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
||||||
|
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||||
|
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||||
|
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
|
||||||
|
import BugTable from '@/views/test-plan/report/detail/component/bugTable.vue';
|
||||||
|
import FeatureCaseTable from '@/views/test-plan/report/detail/component/featureCaseTable.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
editorUploadFile,
|
||||||
|
getReportDetail,
|
||||||
|
planGetShareHref,
|
||||||
|
planReportShare,
|
||||||
|
planReportShareDetail,
|
||||||
|
updateReportDetail,
|
||||||
|
} from '@/api/modules/test-plan/report';
|
||||||
|
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
|
||||||
|
import { defaultReportDetail, statusConfig } from '@/config/testPlan';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { addCommasToNumber } from '@/utils';
|
||||||
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
|
import type { LegendData } from '@/models/apiTest/report';
|
||||||
|
import type { PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
|
||||||
|
import { RouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
|
import { getIndicators } from '@/views/api-test/report/utils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const props = defineProps<{
|
||||||
|
reportId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
|
||||||
|
const showButton = ref(false);
|
||||||
|
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
|
||||||
|
summary: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分享share
|
||||||
|
*/
|
||||||
|
|
||||||
|
const shareLink = ref<string>('');
|
||||||
|
const shareId = ref<string>(route.query.shareId as string);
|
||||||
|
const reportId = ref<string>(props.reportId);
|
||||||
|
const shareLoading = ref<boolean>(false);
|
||||||
|
async function shareHandler() {
|
||||||
|
try {
|
||||||
|
const res = await planReportShare({
|
||||||
|
reportId: reportId.value,
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
});
|
||||||
|
const { origin } = window.location;
|
||||||
|
const hrefShareDetail = await planGetShareHref(res.id);
|
||||||
|
reportId.value = hrefShareDetail.reportId;
|
||||||
|
shareLink.value = `${origin}/#/${RouteEnum.SHARE}/${RouteEnum.SHARE_REPORT_TEST_PLAN}${res.shareUrl}&id=${hrefShareDetail.reportId}`;
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(shareLink.value).then(
|
||||||
|
() => {
|
||||||
|
Message.info(t('bugManagement.detail.shareTip'));
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
Message.error(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.value = shareLink.value;
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(input);
|
||||||
|
Message.info(t('bugManagement.detail.shareTip'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendData = ref<LegendData[]>([]);
|
||||||
|
|
||||||
|
const charOptions = ref({
|
||||||
|
tooltip: {
|
||||||
|
show: false,
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
name: '',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['65%', '80%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center',
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
name: t('common.success'),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#00C261',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
name: t('common.fakeError'),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#FFC14E',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
name: t('common.fail'),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ED0303',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
name: t('common.unExecute'),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#D4D4D8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
name: t('common.block'),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#B379C8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const functionCaseOptions = ref({
|
||||||
|
tooltip: {
|
||||||
|
show: false,
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
name: '',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['65%', '80%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center',
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
name: t('common.success'),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#00C261',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
function initOptionsData() {
|
||||||
|
charOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
||||||
|
return {
|
||||||
|
value: detail.value.executeCount[item.value] || 0,
|
||||||
|
name: t(item.label),
|
||||||
|
itemStyle: {
|
||||||
|
color: item.color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
legendData.value = statusConfig.map((item: StatusListType) => {
|
||||||
|
const rate = (detail.value.executeCount[item.value] / detail.value.caseTotal) * 100;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
label: t(item.label),
|
||||||
|
count: detail.value.executeCount[item.value] || 0,
|
||||||
|
rote: `${Number.isNaN(rate) ? 0 : rate.toFixed(2)}%`,
|
||||||
|
};
|
||||||
|
}) as unknown as LegendData[];
|
||||||
|
|
||||||
|
functionCaseOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
||||||
|
return {
|
||||||
|
value: detail.value.functionalCount[item.value] || 0,
|
||||||
|
name: t(item.label),
|
||||||
|
itemStyle: {
|
||||||
|
color: item.color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDetail() {
|
||||||
|
try {
|
||||||
|
if (shareId.value) {
|
||||||
|
detail.value = await planReportShareDetail(shareId.value, reportId.value);
|
||||||
|
} else {
|
||||||
|
detail.value = await getReportDetail(reportId.value);
|
||||||
|
}
|
||||||
|
richText.value = { summary: detail.value.summary };
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUploadImage(file: File) {
|
||||||
|
const { data } = await editorUploadFile({
|
||||||
|
fileList: [file],
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
getDetail();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleCancel() {
|
||||||
|
richText.value = { summary: detail.value.summary };
|
||||||
|
showButton.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTab = ref('bug');
|
||||||
|
const contentTabList = ref([
|
||||||
|
{
|
||||||
|
value: 'bug',
|
||||||
|
label: t('report.detail.bugDetails'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'featureCase',
|
||||||
|
label: t('report.detail.featureCaseDetails'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
watchEffect(async () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const editorContent = document.querySelector('.editor-content');
|
||||||
|
useEventListener(editorContent, 'click', () => {
|
||||||
|
showButton.value = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (props.reportId) {
|
||||||
|
await getDetail();
|
||||||
|
initOptionsData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.block-title {
|
||||||
|
@apply mb-4 font-medium;
|
||||||
|
}
|
||||||
|
.analysis-wrapper {
|
||||||
|
height: 250px;
|
||||||
|
@apply mb-4 flex items-center;
|
||||||
|
.analysis {
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
|
||||||
|
@apply h-full flex-1 rounded-xl bg-white;
|
||||||
|
.report-analysis {
|
||||||
|
.report-analysis-item {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--color-text-n9);
|
||||||
|
@apply mb-3 flex items-center justify-between;
|
||||||
|
.report-analysis-item-icon {
|
||||||
|
@apply flex items-center;
|
||||||
|
}
|
||||||
|
.report-analysis-item-number {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
.report-analysis-item-unit {
|
||||||
|
color: var(--color-text-4);
|
||||||
|
@apply ml-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.charts {
|
||||||
|
top: 34%;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,449 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<MsCard class="mb-[16px]" hide-back hide-footer auto-height no-content-padding hide-divider>
|
<PlanDetail :report-id="reportId" />
|
||||||
<template #headerLeft>
|
|
||||||
<div v-if="route.query.id" class="flex items-center font-medium"
|
|
||||||
>{{ t('report.name') }}
|
|
||||||
<a-tooltip :content="detail.name" :mouse-enter-delay="300"
|
|
||||||
><div class="one-line-text max-w-[300px]">{{ detail.name }}</div>
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #headerRight>
|
|
||||||
<a-popover position="bottom" content-class="response-popover-content">
|
|
||||||
<div>
|
|
||||||
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
|
|
||||||
{{ detail.executeTime ? dayjs(detail.executeTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
|
|
||||||
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
|
|
||||||
{{ detail.endTime ? dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
|
|
||||||
</div>
|
|
||||||
<template #content>
|
|
||||||
<div class="max-w-[400px] items-center gap-[8px] text-[14px]">
|
|
||||||
<div class="flex-shrink-0 text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
{{ dayjs(detail.executeTime).format('YYYY-MM-DD HH:mm:ss') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-popover>
|
|
||||||
<MsButton
|
|
||||||
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
|
||||||
type="icon"
|
|
||||||
status="secondary"
|
|
||||||
class="ml-4 !rounded-[var(--border-radius-small)]"
|
|
||||||
:loading="shareLoading"
|
|
||||||
@click="shareHandler"
|
|
||||||
>
|
|
||||||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
|
||||||
{{ t('common.share') }}
|
|
||||||
</MsButton>
|
|
||||||
</template>
|
|
||||||
</MsCard>
|
|
||||||
<div class="analysis-wrapper">
|
|
||||||
<div class="analysis">
|
|
||||||
<div>
|
|
||||||
<div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div>
|
|
||||||
<ul class="report-analysis">
|
|
||||||
<li class="report-analysis-item">
|
|
||||||
<div class="report-analysis-item-icon">
|
|
||||||
<svg-icon class="mr-2" width="24px" height="24px" name="threshold" />
|
|
||||||
<span>{{ t('report.detail.threshold') }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="report-analysis-item-number">{{ detail.passThreshold }}</span>
|
|
||||||
<span class="report-analysis-item-unit">(%)</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="report-analysis-item">
|
|
||||||
<div class="report-analysis-item-icon">
|
|
||||||
<svg-icon class="mr-2" width="24px" height="24px" name="passRate" />
|
|
||||||
<span>{{ t('report.detail.reportPassRate') }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="report-analysis-item-number">{{ detail.passRate }}</span>
|
|
||||||
<span class="report-analysis-item-unit"> (%)</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="report-analysis-item">
|
|
||||||
<div class="report-analysis-item-icon">
|
|
||||||
<svg-icon class="mr-2" width="24px" height="24px" name="passRate" />
|
|
||||||
<span>{{ t('report.detail.performCompletion') }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="report-analysis-item-number">{{ detail.executeRate }}</span>
|
|
||||||
<span class="report-analysis-item-unit">(%)</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="report-analysis-item">
|
|
||||||
<div class="report-analysis-item-icon">
|
|
||||||
<svg-icon class="mr-2" width="24px" height="24px" name="bugTotal" />
|
|
||||||
<span>{{ t('report.detail.totalDefects') }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="report-analysis-item-number">{{ addCommasToNumber(detail.bugCount) }}</span>
|
|
||||||
<span class="report-analysis-item-unit">({{ t('report.detail.number') }})</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="analysis mx-4">
|
|
||||||
<div>
|
|
||||||
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
|
|
||||||
<SetReportChart
|
|
||||||
size="160px"
|
|
||||||
offset="top-[34%] right-0 bottom-0 left-0"
|
|
||||||
:legend-data="legendData"
|
|
||||||
:options="charOptions"
|
|
||||||
:request-total="getIndicators(detail.caseTotal) || 0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="analysis">
|
|
||||||
<div>
|
|
||||||
<div class="block-title">{{ t('report.detail.useCaseAnalysis') }}</div>
|
|
||||||
<div class="flex">
|
|
||||||
<div class="w-[70%]">
|
|
||||||
<SingleStatusProgress :detail="detail" status="pending" />
|
|
||||||
<SingleStatusProgress :detail="detail" status="success" />
|
|
||||||
<SingleStatusProgress :detail="detail" status="block" />
|
|
||||||
<SingleStatusProgress :detail="detail" status="error" />
|
|
||||||
</div>
|
|
||||||
<div class="relative w-[30%]">
|
|
||||||
<div class="charts absolute w-full text-center">
|
|
||||||
<div class="text-[12px] !text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
|
|
||||||
<a-popover position="bottom" content-class="response-popover-content">
|
|
||||||
<div class="flex justify-center text-[18px] font-medium">
|
|
||||||
<div class="one-line-text max-w-[60px] text-[var(--color-text-1)]">{{ detail.passRate }}% </div>
|
|
||||||
</div>
|
|
||||||
<template #content>
|
|
||||||
<div class="min-w-[95px] max-w-[400px] p-4 text-[14px]">
|
|
||||||
<div class="text-[12px] font-medium text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
|
|
||||||
<div class="mt-2 text-[18px] font-medium text-[var(--color-text-1)]">{{ detail.passRate }} %</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-popover>
|
|
||||||
</div>
|
|
||||||
<div class="flex h-full w-full items-center justify-center">
|
|
||||||
<MsChart width="120px" height="120px" :options="functionCaseOptions"
|
|
||||||
/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MsCard class="mb-[16px]" simple auto-height>
|
|
||||||
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
|
|
||||||
<MsRichText
|
|
||||||
v-model:raw="richText.summary"
|
|
||||||
v-model:filedIds="richText.richTextTmpFileIds"
|
|
||||||
:upload-image="handleUploadImage"
|
|
||||||
:preview-url="PreviewEditorImageUrl"
|
|
||||||
class="mt-[8px] w-full"
|
|
||||||
/>
|
|
||||||
<div v-show="showButton" 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>
|
|
||||||
</MsCard>
|
|
||||||
<MsCard simple auto-height>
|
|
||||||
<MsTab
|
|
||||||
v-model:active-key="activeTab"
|
|
||||||
:show-badge="false"
|
|
||||||
:content-tab-list="contentTabList"
|
|
||||||
no-content
|
|
||||||
class="relative mb-[16px] border-b"
|
|
||||||
/>
|
|
||||||
<BugTable v-if="activeTab === 'bug'" :report-id="reportId" :share-id="shareId" />
|
|
||||||
<FeatureCaseTable v-if="activeTab === 'featureCase'" :report-id="reportId" :share-id="shareId" />
|
|
||||||
</MsCard>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useEventListener } from '@vueuse/core';
|
|
||||||
import { Message } from '@arco-design/web-vue';
|
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import MsChart from '@/components/pure/chart/index.vue';
|
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
|
||||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
|
||||||
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
|
||||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
|
||||||
import SingleStatusProgress from '../component/singleStatusProgress.vue';
|
|
||||||
import BugTable from './component/bugTable.vue';
|
|
||||||
import FeatureCaseTable from './component/featureCaseTable.vue';
|
|
||||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
|
||||||
|
|
||||||
import { editorUploadFile, getReportDetail, updateReportDetail } from '@/api/modules/test-plan/report';
|
|
||||||
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
|
|
||||||
import { defaultReportDetail, statusConfig } from '@/config/testPlan';
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
|
||||||
import { addCommasToNumber } from '@/utils';
|
|
||||||
|
|
||||||
import type { LegendData } from '@/models/apiTest/report';
|
|
||||||
import type { PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
|
|
||||||
|
|
||||||
import { getIndicators } from '@/views/api-test/report/utils';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const reportId = ref<string>(route.query.id as string);
|
const reportId = ref<string>(route.query.id as string);
|
||||||
const shareId = ref<string>(route.query.shareId as string);
|
|
||||||
|
|
||||||
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
|
|
||||||
const showButton = ref(false);
|
|
||||||
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
|
|
||||||
summary: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 分享
|
|
||||||
const shareLoading = ref<boolean>(false);
|
|
||||||
|
|
||||||
function shareHandler() {}
|
|
||||||
|
|
||||||
const legendData = ref<LegendData[]>([]);
|
|
||||||
|
|
||||||
const charOptions = ref({
|
|
||||||
tooltip: {
|
|
||||||
show: false,
|
|
||||||
trigger: 'item',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
series: {
|
|
||||||
name: '',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['65%', '80%'],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
position: 'center',
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: 0,
|
|
||||||
name: t('common.success'),
|
|
||||||
itemStyle: {
|
|
||||||
color: '#00C261',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0,
|
|
||||||
name: t('common.fakeError'),
|
|
||||||
itemStyle: {
|
|
||||||
color: '#FFC14E',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0,
|
|
||||||
name: t('common.fail'),
|
|
||||||
itemStyle: {
|
|
||||||
color: '#ED0303',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0,
|
|
||||||
name: t('common.unExecute'),
|
|
||||||
itemStyle: {
|
|
||||||
color: '#D4D4D8',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0,
|
|
||||||
name: t('common.block'),
|
|
||||||
itemStyle: {
|
|
||||||
color: '#B379C8',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const functionCaseOptions = ref({
|
|
||||||
tooltip: {
|
|
||||||
show: false,
|
|
||||||
trigger: 'item',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
series: {
|
|
||||||
name: '',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['65%', '80%'],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
position: 'center',
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: 0,
|
|
||||||
name: t('common.success'),
|
|
||||||
itemStyle: {
|
|
||||||
color: '#00C261',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化图表
|
|
||||||
function initOptionsData() {
|
|
||||||
charOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
|
||||||
return {
|
|
||||||
value: detail.value.executeCount[item.value] || 0,
|
|
||||||
name: t(item.label),
|
|
||||||
itemStyle: {
|
|
||||||
color: item.color,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
legendData.value = statusConfig.map((item: StatusListType) => {
|
|
||||||
const rate = (detail.value.executeCount[item.value] / detail.value.caseTotal) * 100;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
label: t(item.label),
|
|
||||||
count: detail.value.executeCount[item.value] || 0,
|
|
||||||
rote: `${Number.isNaN(rate) ? 0 : rate.toFixed(2)}%`,
|
|
||||||
};
|
|
||||||
}) as unknown as LegendData[];
|
|
||||||
|
|
||||||
functionCaseOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
|
||||||
return {
|
|
||||||
value: detail.value.functionalCount[item.value] || 0,
|
|
||||||
name: t(item.label),
|
|
||||||
itemStyle: {
|
|
||||||
color: item.color,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDetail() {
|
|
||||||
try {
|
|
||||||
detail.value = await getReportDetail(reportId.value);
|
|
||||||
richText.value = { summary: detail.value.summary };
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
nextTick(() => {
|
|
||||||
const editorContent = document.querySelector('.editor-content');
|
|
||||||
useEventListener(editorContent, 'click', () => {
|
|
||||||
showButton.value = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await getDetail();
|
|
||||||
initOptionsData();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleUploadImage(file: File) {
|
|
||||||
const { data } = await editorUploadFile({
|
|
||||||
fileList: [file],
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
await getDetail();
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleCancel() {
|
|
||||||
richText.value = { summary: detail.value.summary };
|
|
||||||
showButton.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeTab = ref('bug');
|
|
||||||
const contentTabList = ref([
|
|
||||||
{
|
|
||||||
value: 'bug',
|
|
||||||
label: t('report.detail.bugDetails'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'featureCase',
|
|
||||||
label: t('report.detail.featureCaseDetails'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less"></style>
|
||||||
.block-title {
|
|
||||||
@apply mb-4 font-medium;
|
|
||||||
}
|
|
||||||
.analysis-wrapper {
|
|
||||||
height: 250px;
|
|
||||||
@apply mb-4 flex items-center;
|
|
||||||
.analysis {
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
|
|
||||||
@apply h-full flex-1 rounded-xl bg-white;
|
|
||||||
.report-analysis {
|
|
||||||
.report-analysis-item {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--color-text-n9);
|
|
||||||
@apply mb-3 flex items-center justify-between;
|
|
||||||
.report-analysis-item-icon {
|
|
||||||
@apply flex items-center;
|
|
||||||
}
|
|
||||||
.report-analysis-item-number {
|
|
||||||
@apply font-medium;
|
|
||||||
}
|
|
||||||
.report-analysis-item-unit {
|
|
||||||
color: var(--color-text-4);
|
|
||||||
@apply ml-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.charts {
|
|
||||||
top: 34%;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 99;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.rich-wrapper) .halo-rich-text-editor .ProseMirror {
|
|
||||||
height: 58px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<PlanDetail :report-id="reportId" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
|
||||||
|
|
||||||
|
import { planGetShareHref } from '@/api/modules/test-plan/report';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const shareId = ref<string>(route.query.shareId as string);
|
||||||
|
const reportId = ref<string>(route.query.id as string);
|
||||||
|
|
||||||
|
async function getShareDetail() {
|
||||||
|
try {
|
||||||
|
const hrefShareDetail = await planGetShareHref(shareId.value);
|
||||||
|
reportId.value = hrefShareDetail.reportId;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getShareDetail();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@ -108,6 +108,11 @@
|
|||||||
<div class="one-line-text">{{ characterLimit(record.createUserName) }}</div>
|
<div class="one-line-text">{{ characterLimit(record.createUserName) }}</div>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
<template #createTime="{ record }">
|
||||||
|
<a-tooltip :content="`${dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss')}`" position="tl">
|
||||||
|
<div class="one-line-text">{{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
<template #moduleId="{ record }">
|
<template #moduleId="{ record }">
|
||||||
<a-tooltip :content="getModules(record.moduleId, props.moduleTree)" position="top">
|
<a-tooltip :content="getModules(record.moduleId, props.moduleTree)" position="top">
|
||||||
<span class="one-line-text inline-block">
|
<span class="one-line-text inline-block">
|
||||||
@ -268,6 +273,7 @@
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
|
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
|
||||||
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
|
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
|
||||||
@ -420,7 +426,6 @@
|
|||||||
{
|
{
|
||||||
title: 'testPlan.testPlanIndex.createTime',
|
title: 'testPlan.testPlanIndex.createTime',
|
||||||
slotName: 'createTime',
|
slotName: 'createTime',
|
||||||
dataIndex: 'createTime',
|
|
||||||
showInTable: true,
|
showInTable: true,
|
||||||
sortable: {
|
sortable: {
|
||||||
sortDirections: ['ascend', 'descend'],
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
@ -100,14 +100,6 @@
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (detailCount.value.passRate > detailCount.value.passThreshold) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
percentage: 100,
|
|
||||||
color: 'rgb(var(--success-6))',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
percentage: (successCount / caseTotal) * 100,
|
percentage: (successCount / caseTotal) * 100,
|
||||||
|
Loading…
Reference in New Issue
Block a user