feat(工作台): 工作台联调部分
This commit is contained in:
parent
dc58054aef
commit
a8e718a961
|
@ -7,8 +7,17 @@ import type { ReviewItem } from '@/models/caseManagement/caseReview';
|
|||
import type { CaseManagementTable } from '@/models/caseManagement/featureCase';
|
||||
import type { CommonList, TableQueryParams } from '@/models/common';
|
||||
import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan';
|
||||
import type {
|
||||
OverViewOfProject,
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
WorkHomePageDetail,
|
||||
} from '@/models/workbench/homePage';
|
||||
|
||||
import {
|
||||
EditDashboardLayoutUrl,
|
||||
GetDashboardLayoutUrl,
|
||||
WorkAssociateCaseDetailUrl,
|
||||
WorkbenchApiCaseListUrl,
|
||||
WorkbenchBugListUrl,
|
||||
WorkbenchCaseListUrl,
|
||||
|
@ -16,6 +25,13 @@ import {
|
|||
WorkbenchScenarioListUrl,
|
||||
WorkbenchTestPlanListUrl,
|
||||
WorkbenchTestPlanStatisticUrl,
|
||||
WorkCaseCountDetailUrl,
|
||||
WorkMemberViewDetailUrl,
|
||||
WorkMyCreatedDetailUrl,
|
||||
WorkProOverviewDetailUrl,
|
||||
WorkTodoBugListUrl,
|
||||
WorkTodoPlanListUrl,
|
||||
WorkTodoReviewListUrl,
|
||||
} from '../requrls/workbench';
|
||||
|
||||
// 我的-场景列表
|
||||
|
@ -52,3 +68,49 @@ export function workbenchBugList(data: TableQueryParams) {
|
|||
export function workbenchApiCaseList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<ApiCaseDetail>>({ url: WorkbenchApiCaseListUrl, data });
|
||||
}
|
||||
|
||||
// 工作台首页概览
|
||||
export function workProOverviewDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<OverViewOfProject>({ url: WorkProOverviewDetailUrl, data });
|
||||
}
|
||||
// 我创建的
|
||||
export function workMyCreatedDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<OverViewOfProject>({ url: WorkMyCreatedDetailUrl, data });
|
||||
}
|
||||
// 人员概览
|
||||
export function workMemberViewDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<OverViewOfProject>({ url: WorkMemberViewDetailUrl, data });
|
||||
}
|
||||
// 获取用户布局
|
||||
export function getDashboardLayout(orgId: string) {
|
||||
return MSR.get<SelectedCardItem[]>({ url: `${GetDashboardLayoutUrl}/${orgId}` });
|
||||
}
|
||||
|
||||
// 获取用户布局
|
||||
export function editDashboardLayout(data: SelectedCardItem[], orgId: string) {
|
||||
return MSR.post({ url: `${EditDashboardLayoutUrl}/${orgId}`, data });
|
||||
}
|
||||
|
||||
// 工作台-首页-用例数
|
||||
export function workCaseCountDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<PassRateDataType>({ url: WorkCaseCountDetailUrl, data });
|
||||
}
|
||||
// 工作台-首页-关联用例数
|
||||
export function workAssociateCaseDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<PassRateDataType>({ url: WorkAssociateCaseDetailUrl, data });
|
||||
}
|
||||
|
||||
// 待办-用例评审列表
|
||||
export function workbenchTodoReviewList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<ReviewItem>>({ url: WorkTodoReviewListUrl, data });
|
||||
}
|
||||
|
||||
// 待办-缺陷列表
|
||||
export function workbenchTodoBugList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<BugListItem>>({ url: WorkTodoBugListUrl, data });
|
||||
}
|
||||
|
||||
// 待办-测试计划列表
|
||||
export function workbenchTodoTestPlanList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<TestPlanItem>>({ url: WorkTodoPlanListUrl, data });
|
||||
}
|
||||
|
|
|
@ -5,3 +5,13 @@ export const WorkbenchTestPlanStatisticUrl = '/dashboard/my/plan/statistics'; //
|
|||
export const WorkbenchCaseListUrl = '/dashboard/my/functional/page'; // 工作台-我的-用例列表
|
||||
export const WorkbenchBugListUrl = '/dashboard/my/bug/page'; // 工作台-我的-缺陷列表
|
||||
export const WorkbenchApiCaseListUrl = '/dashboard/my/api/page'; // 工作台-我的-接口用例列表
|
||||
export const WorkProOverviewDetailUrl = '/dashboard/project_view'; // 工作台首页项目概览
|
||||
export const WorkMyCreatedDetailUrl = '/dashboard/create_by_me'; // 工作台我创建的
|
||||
export const GetDashboardLayoutUrl = '/dashboard/layout/get'; // 获取用户布局
|
||||
export const EditDashboardLayoutUrl = '/dashboard/layout/edit'; // 更新用户布局
|
||||
export const WorkTodoPlanListUrl = '/dashboard/todo/plan/page'; // 工作台-待办-测试计划列表
|
||||
export const WorkTodoReviewListUrl = '/dashboard/todo/review/page'; // 工作台-待办-用例评审
|
||||
export const WorkTodoBugListUrl = '/dashboard/todo/bug/page'; // 工作台-待办-缺陷列表
|
||||
export const WorkMemberViewDetailUrl = '/dashboard/project_member_view'; // 工作台-首页-人员概览
|
||||
export const WorkCaseCountDetailUrl = '/dashboard/case_count'; // 工作台-首页-用例数量
|
||||
export const WorkAssociateCaseDetailUrl = '/dashboard/associate_case_count'; // 工作台-首页-关联用例数量
|
||||
|
|
|
@ -259,6 +259,7 @@ export default function useTableProps<T>(
|
|||
return data;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO 在这里处理拦截设置表格无资源权限
|
||||
setTableErrorStatus('error');
|
||||
propsRes.value.data = [];
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { TableQueryParams } from '@/models/common';
|
||||
import { WorkCardEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
// 配置卡片列表
|
||||
|
@ -27,3 +28,55 @@ export interface SelectedCardItem {
|
|||
projectIds: string[];
|
||||
handleUsers: string[];
|
||||
}
|
||||
|
||||
// 查询入参
|
||||
export interface WorkHomePageDetail extends TableQueryParams {
|
||||
dayNumber: number | null;
|
||||
startTime: number | null;
|
||||
endTime: number | null;
|
||||
projectIds: string[];
|
||||
handleUsers?: string[];
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface TimeFormParams {
|
||||
dayNumber: number | null;
|
||||
startTime: number | null;
|
||||
endTime: number | null;
|
||||
}
|
||||
|
||||
export interface OverViewOfProject {
|
||||
caseCountMap: Record<string, number>; // 模块列表
|
||||
projectCountList: {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number[];
|
||||
}[]; // 项目列表
|
||||
xaxis: string[]; // 横坐标
|
||||
}
|
||||
|
||||
export interface ModuleCardItem {
|
||||
label: string | number;
|
||||
value: string | number;
|
||||
count?: number;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type StatusStatisticsMapType = Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
count: number;
|
||||
}[]
|
||||
>;
|
||||
|
||||
export interface PassRateDataType {
|
||||
statusStatisticsMap: StatusStatisticsMapType;
|
||||
statusPercentList: {
|
||||
status: string; // 状态
|
||||
count: number;
|
||||
percentValue: string; // 百分比
|
||||
}[];
|
||||
}
|
||||
|
|
|
@ -277,8 +277,13 @@ const useAppStore = defineStore('app', {
|
|||
try {
|
||||
const { isWhiteListPage } = useUser();
|
||||
const routeName = router.currentRoute.value.name as string;
|
||||
if (!this.currentProjectId || routeName?.includes('setting') || isWhiteListPage()) {
|
||||
// 如果没有项目id,或访问的是系统设置下的页面/白名单页面,则不读取项目基础信息
|
||||
if (
|
||||
!this.currentProjectId ||
|
||||
routeName?.includes('setting') ||
|
||||
routeName?.includes('workstation') ||
|
||||
isWhiteListPage()
|
||||
) {
|
||||
// 如果没有项目id,或访问的是系统设置下的页面/白名单/工作台页面,则不读取项目基础信息
|
||||
return;
|
||||
}
|
||||
const res = await getProjectInfo(this.currentProjectId);
|
||||
|
|
|
@ -244,18 +244,19 @@ const useUserStore = defineStore('user', {
|
|||
const { isLoginPage } = useUser();
|
||||
const appStore = useAppStore();
|
||||
const isLogin = await this.isLogin(forceSet);
|
||||
const routeName = router.currentRoute.value.name as string;
|
||||
if (isLogin && appStore.currentProjectId !== 'no_such_project') {
|
||||
// 当前为登陆状态,且已经选择了项目,初始化当前项目配置
|
||||
try {
|
||||
const HasProjectPermission = await getUserHasProjectPermission(appStore.currentProjectId);
|
||||
if (!HasProjectPermission) {
|
||||
// 无权限&&工作台不会跳转无资源
|
||||
if (!HasProjectPermission && !routeName?.includes('workstation')) {
|
||||
// 没有项目权限(用户所在的当前项目被禁用&用户被移除出去该项目)
|
||||
router.push({
|
||||
name: NO_PROJECT_ROUTE_NAME,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const routeName = router.currentRoute.value.name as string;
|
||||
if (routeName?.includes('setting')) {
|
||||
// 访问系统设置下的页面,不需要获取项目信息,会在切换到非系统设置页面时获取(ms-menu组件内初始化会获取)
|
||||
appStore.setCurrentMenuConfig([]);
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
|
||||
import { getCustomFieldHeader, getCustomOptionHeader } from '@/api/modules/bug-management';
|
||||
import { workbenchBugList } from '@/api/modules/workbench';
|
||||
import { workbenchBugList, workbenchTodoBugList } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
@ -192,8 +192,12 @@
|
|||
columns.splice(2, 0, ...customColumns);
|
||||
await initFilterOptions();
|
||||
|
||||
const workbenchBugPage = computed(() => {
|
||||
return props.type === 'my_todo' ? workbenchTodoBugList : workbenchBugList;
|
||||
});
|
||||
|
||||
const { propsRes, propsEvent, setLoadListParams, loadList } = useTable(
|
||||
workbenchBugList,
|
||||
workbenchBugPage.value,
|
||||
{
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
|
@ -237,6 +241,7 @@
|
|||
setLoadListParams({
|
||||
projectId: props.project,
|
||||
viewId: props.type,
|
||||
myTodo: props.type === 'my_todo',
|
||||
});
|
||||
loadList();
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
|
||||
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
|
||||
|
||||
import { workbenchReviewList } from '@/api/modules/workbench';
|
||||
import { workbenchReviewList, workbenchTodoReviewList } from '@/api/modules/workbench';
|
||||
import { reviewStatusMap } from '@/config/caseManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
|
@ -156,7 +156,11 @@
|
|||
},
|
||||
];
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workbenchReviewList, {
|
||||
const workbenchReviewPage = computed(() => {
|
||||
return props.type === 'my_todo' ? workbenchTodoReviewList : workbenchReviewList;
|
||||
});
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workbenchReviewPage.value, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
showSetting: false,
|
||||
|
@ -179,6 +183,7 @@
|
|||
setLoadListParams({
|
||||
projectId: props.project,
|
||||
viewId: props.type,
|
||||
myTodo: props.type === 'my_todo',
|
||||
});
|
||||
loadList();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MsCard v-if="props.allScreen" class="mb-[16px]" simple>
|
||||
<MsCard v-if="props.allScreen" simple :special-height="36">
|
||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||
<div class="no-config-svg"></div>
|
||||
<div class="flex items-center">
|
||||
|
@ -14,15 +14,21 @@
|
|||
class="ml-[8px] font-medium"
|
||||
@click="() => emit('config')"
|
||||
>
|
||||
{{ t('workbench.homePage.configureWorkbench') }}
|
||||
{{ t('workbench.homePage.cardSetting') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</div>
|
||||
</MsCard>
|
||||
<div v-else-if="props.isDashboard" class="no-card">
|
||||
<div class="no-card-svg"></div>
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('workbench.homePage.noCard') }}</div>
|
||||
<div class="text-[var(--color-text-4)]">{{ t('workbench.homePage.noCardDesc') }}</div>
|
||||
<div v-else-if="props.noResPermission || props.isDashboard" :class="`${props.height || 'h-full'} w-full`">
|
||||
<div class="no-card">
|
||||
<div :class="`${props.noResPermission ? 'no-permission-svg' : 'no-card-svg'}`"></div>
|
||||
<div class="font-medium text-[var(--color-text-1)]">
|
||||
{{ props.noResPermission ? t('workbench.homePage.workNoProjectTip') : t('workbench.homePage.noCard') }}
|
||||
</div>
|
||||
<div v-if="!props.noResPermission" class="text-[var(--color-text-4)]">
|
||||
{{ t('workbench.homePage.noCardDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="not-setting-data">
|
||||
{{ t('workbench.homePage.noDataTemporarily') }}
|
||||
|
@ -39,6 +45,8 @@
|
|||
const props = defineProps<{
|
||||
allScreen?: boolean;
|
||||
isDashboard?: boolean; // 是否仪表盘
|
||||
noResPermission?: boolean; // 无资源权限
|
||||
height?: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -65,7 +73,6 @@
|
|||
background-size: cover;
|
||||
}
|
||||
.no-card {
|
||||
margin-top: -10%;
|
||||
@apply flex h-full w-full flex-col items-center justify-center gap-4;
|
||||
.no-card-svg {
|
||||
margin: 0 auto;
|
||||
|
@ -74,5 +81,12 @@
|
|||
background: url('@/assets/svg/work-no-card.svg');
|
||||
background-size: cover;
|
||||
}
|
||||
.no-permission-svg {
|
||||
margin: 0 auto;
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
background: url('@/assets/svg/no_resource.svg');
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -129,7 +129,11 @@
|
|||
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
|
||||
import StatusProgress from '@/views/test-plan/testPlan/components/statusProgress.vue';
|
||||
|
||||
import { workbenchTestPlanList, workbenchTestPlanStatistic } from '@/api/modules/workbench';
|
||||
import {
|
||||
workbenchTestPlanList,
|
||||
workbenchTestPlanStatistic,
|
||||
workbenchTodoTestPlanList,
|
||||
} from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
|
||||
|
@ -246,7 +250,11 @@
|
|||
showSelectorAll: false,
|
||||
});
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workbenchTestPlanList, tableProps.value);
|
||||
const getTestPlanList = computed(() => {
|
||||
return props.type === 'my_todo' ? workbenchTodoTestPlanList : workbenchTestPlanList;
|
||||
});
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getTestPlanList.value, tableProps.value);
|
||||
|
||||
const planData = computed(() => {
|
||||
return propsRes.value.data;
|
||||
|
@ -293,6 +301,7 @@
|
|||
type: showType.value,
|
||||
projectId: props.project,
|
||||
viewId: props.type,
|
||||
myTodo: props.type === 'my_todo',
|
||||
});
|
||||
loadList();
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title">
|
||||
{{
|
||||
props.type === WorkCardEnum.API_CASE_COUNT
|
||||
? t('workbench.homePage.apiUseCasesNumber')
|
||||
: t('workbench.homePage.scenarioUseCasesNumber')
|
||||
}}
|
||||
{{ t(props.item.label) }}
|
||||
</div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -26,23 +21,15 @@
|
|||
<div class="mt-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<div class="case-count-item-title">{{ t('workbench.homePage.executionTimes') }}</div>
|
||||
<div class="case-count-item-number">{{ addCommasToNumber(executionTimes) }}</div>
|
||||
<div v-for="(ele, index) of executionTimeValue" :key="index" class="case-count-item-content">
|
||||
<div class="case-count-item-title">{{ ele.name }}</div>
|
||||
<div class="case-count-item-number">{{ addCommasToNumber(ele.count) }}</div>
|
||||
</div>
|
||||
<div class="case-count-item flex">
|
||||
<div class="case-count-item-count">
|
||||
<div class="case-count-item-title">
|
||||
{{
|
||||
props.type === WorkCardEnum.API_CASE_COUNT
|
||||
? t('workbench.homePage.apiUseCasesNumber')
|
||||
: t('workbench.homePage.scenarioUseCasesNumber')
|
||||
}}
|
||||
</div>
|
||||
<div class="case-count-item-number">{{ addCommasToNumber(executionTimes) }}</div>
|
||||
</div>
|
||||
<div class="case-count-item-count">
|
||||
<div class="case-count-item-title">{{ t('workbench.homePage.misstatementCount') }}</div>
|
||||
<div class="case-count-item-number">{{ addCommasToNumber(executionTimes) }}</div>
|
||||
<div class="case-count-item">
|
||||
<div v-for="(ele, index) of apiCountValue" :key="index" class="case-count-item-content">
|
||||
<div class="case-count-item-title">{{ ele.name }}</div>
|
||||
<div class="case-count-item-number">{{ addCommasToNumber(ele.count) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,66 +61,82 @@
|
|||
import useAppStore from '@/store/modules/app';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const projectIds = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
type: WorkCardEnum;
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const executionTimes = ref(100000);
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const executionTimeValue = ref<{ name: string; count: number }[]>([
|
||||
{
|
||||
name: '执行次数',
|
||||
count: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
const apiCountValue = ref<{ name: string; count: number }[]>([
|
||||
{
|
||||
name:
|
||||
props.item.key === WorkCardEnum.API_CASE_COUNT
|
||||
? t('workbench.homePage.apiUseCasesNumber')
|
||||
: t('workbench.homePage.scenarioUseCasesNumber'),
|
||||
count: 100,
|
||||
},
|
||||
{
|
||||
name: t('workbench.homePage.misstatementCount'),
|
||||
count: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
// 接口覆盖
|
||||
const coverData = ref([
|
||||
const coverData = ref<{ name: string; value: number }[]>([
|
||||
{
|
||||
value: 0,
|
||||
name: t('workbench.homePage.notCover'),
|
||||
itemStyle: {
|
||||
color: '#D4D4D8',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('workbench.homePage.covered'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const caseExecuteData = ref([
|
||||
const caseExecuteData = ref<{ name: string; value: number }[]>([
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.unExecute'),
|
||||
itemStyle: {
|
||||
color: '#D4D4D8',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.executed'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const casePassData = ref([
|
||||
const casePassData = ref<{ name: string; value: number }[]>([
|
||||
{
|
||||
value: 0,
|
||||
name: t('workbench.homePage.notPass'),
|
||||
itemStyle: {
|
||||
color: '#ED0303',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('workbench.homePage.havePassed'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -141,30 +144,71 @@
|
|||
return {
|
||||
name: t('workbench.homePage.apiCoverage'),
|
||||
count: '80%',
|
||||
color: ['#EDEDF1', '#00C261'],
|
||||
};
|
||||
});
|
||||
|
||||
const executeTitleConfig = computed(() => {
|
||||
return props.type === WorkCardEnum.API_CASE_COUNT
|
||||
return props.item.key === WorkCardEnum.API_CASE_COUNT
|
||||
? {
|
||||
name: t('workbench.homePage.caseExecutionRate'),
|
||||
count: '80%',
|
||||
color: ['#EDEDF1', '#00C261'],
|
||||
}
|
||||
: {
|
||||
name: t('workbench.homePage.sceneExecutionRate'),
|
||||
count: '80%',
|
||||
color: ['#EDEDF1', '#00C261'],
|
||||
};
|
||||
});
|
||||
|
||||
const casePassTitleConfig = computed(() => {
|
||||
return props.type === WorkCardEnum.API_CASE_COUNT
|
||||
return props.item.key === WorkCardEnum.API_CASE_COUNT
|
||||
? {
|
||||
name: t('workbench.homePage.casePassedRate'),
|
||||
count: '80%',
|
||||
color: ['#00C261', '#ED0303'],
|
||||
}
|
||||
: {
|
||||
name: t('workbench.homePage.executionRate'),
|
||||
count: '80%',
|
||||
color: ['#00C261', '#ED0303'],
|
||||
};
|
||||
});
|
||||
|
||||
function initApiOrScenarioCount() {}
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initApiOrScenarioCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initApiOrScenarioCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
@ -174,6 +218,8 @@
|
|||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-text-n9);
|
||||
@apply flex items-center;
|
||||
.case-count-item-content {
|
||||
@apply flex-1;
|
||||
.case-count-item-count {
|
||||
@apply flex-1;
|
||||
|
@ -189,6 +235,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.case-ratio-wrapper {
|
||||
@apply flex;
|
||||
.case-ratio-item {
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title">
|
||||
{{ t('workbench.homePage.interfaceChange') }}
|
||||
{{ t(props.item.label) }}
|
||||
</div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -53,12 +52,29 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const projectIds = ref('');
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
|
@ -119,11 +135,57 @@
|
|||
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
})
|
||||
);
|
||||
function initData() {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
setLoadListParams({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
});
|
||||
loadList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setLoadListParams({});
|
||||
loadList();
|
||||
initData();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initData();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div class="card-wrapper api-count-wrapper">
|
||||
<div class="card-wrapper api-count-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.apiCount') }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -18,15 +17,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="coverValueList" />
|
||||
<TabCard :content-tab-list="apiCountTabList" not-has-padding hidden-border min-width="270px">
|
||||
<template #item="{ item: tabItem }">
|
||||
<div class="w-full">
|
||||
<PassRatePie
|
||||
:tooltip-text="tabItem.tooltip"
|
||||
:options="tabItem.options"
|
||||
:size="60"
|
||||
:value-list="tabItem.valueList"
|
||||
/>
|
||||
</div>
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="passValueList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px] h-[148px]">
|
||||
</template>
|
||||
</TabCard>
|
||||
<div class="h-[148px]">
|
||||
<MsChart :options="apiCountOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,53 +41,68 @@
|
|||
* @desc 接口数量
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
import TabCard from './tabCard.vue';
|
||||
|
||||
import { commonConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import type {
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
StatusStatisticsMapType,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
projectIds: string[];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const projectIds = ref('');
|
||||
const projectOptions = ref<SelectOptionData[]>([]);
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const options = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
const options = ref(cloneDeep(commonRatePieOptions));
|
||||
|
||||
// TODO 假数据
|
||||
const detail = ref<PassRateDataType>({
|
||||
statusStatisticsMap: {
|
||||
cover: [
|
||||
{ name: '覆盖率', count: 10 },
|
||||
{ name: '已覆盖', count: 2 },
|
||||
{ name: '未覆盖', count: 1 },
|
||||
],
|
||||
success: [
|
||||
{ name: '覆盖率', count: 10 },
|
||||
{ name: '已覆盖', count: 2 },
|
||||
{ name: '未覆盖', count: 1 },
|
||||
],
|
||||
},
|
||||
statusPercentList: [
|
||||
{ status: 'HTTP', count: 1, percentValue: '10%' },
|
||||
{ status: 'TCP', count: 3, percentValue: '0%' },
|
||||
{ status: 'BBB', count: 6, percentValue: '0%' },
|
||||
],
|
||||
});
|
||||
|
||||
const coverValueList = ref([
|
||||
|
@ -111,148 +129,114 @@
|
|||
value: 2000,
|
||||
},
|
||||
]);
|
||||
|
||||
const apiCountOptions = ref({
|
||||
title: {
|
||||
show: true,
|
||||
text: '总数(个)',
|
||||
left: 60,
|
||||
top: '38px',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
color: '#959598',
|
||||
},
|
||||
subtext: '100111',
|
||||
subtextStyle: {
|
||||
fontSize: 20,
|
||||
color: '#323233',
|
||||
fontWeight: 'bold',
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
color: ['#811FA3', '#00C261', '#3370FF', '#FFA1FF', '#EE50A3', '#FF9964', '#F9F871', '#C3DD40'],
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
position: 'right',
|
||||
},
|
||||
legend: {
|
||||
width: '100%',
|
||||
height: 128,
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
pageButtonItemGap: 5,
|
||||
pageButtonGap: 5,
|
||||
pageIconColor: '#00000099',
|
||||
pageIconInactiveColor: '#00000042',
|
||||
pageIconSize: [7, 5],
|
||||
pageTextStyle: {
|
||||
color: '#00000099',
|
||||
fontSize: 12,
|
||||
},
|
||||
pageButtonPosition: 'end',
|
||||
itemGap: 16,
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
icon: 'circle',
|
||||
bottom: 'center',
|
||||
left: 180,
|
||||
formatter: (name: any) => {
|
||||
return `{a|${name}} {b|${addCommasToNumber(1022220)}} {c|${10}}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#333',
|
||||
fontSize: 14, // 字体大小
|
||||
textBorderType: 'solid',
|
||||
rich: {
|
||||
a: {
|
||||
width: 50,
|
||||
color: '#959598',
|
||||
fontSize: 12,
|
||||
align: 'left',
|
||||
},
|
||||
b: {
|
||||
width: 50,
|
||||
color: '#323233',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
align: 'right',
|
||||
},
|
||||
c: {
|
||||
width: 50,
|
||||
color: '#323233',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
align: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
media: [
|
||||
const coverOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const completeOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const apiCountTabList = computed(() => {
|
||||
return [
|
||||
{
|
||||
query: { maxWidth: 600 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
label: '',
|
||||
value: 'execution',
|
||||
valueList: coverValueList.value,
|
||||
options: { ...coverOptions.value },
|
||||
tooltip: 'workbench.homePage.apiCountCoverRateTooltip',
|
||||
},
|
||||
{
|
||||
query: { minWidth: 601, maxWidth: 800 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 450,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: { minWidth: 801, maxWidth: 1200 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: { minWidth: 1201 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['75%', '90%'],
|
||||
center: [90, '48%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
label: '',
|
||||
value: 'pass',
|
||||
valueList: passValueList.value,
|
||||
options: { ...completeOptions.value },
|
||||
tooltip: 'workbench.homePage.apiCountCompleteRateTooltip',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const apiCountOptions = ref({});
|
||||
|
||||
function handlePassRatePercent(data: { name: string; count: number }[]) {
|
||||
return data.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
|
||||
const { cover, success } = statusStatisticsMap;
|
||||
coverValueList.value = handlePassRatePercent(cover);
|
||||
passValueList.value = handlePassRatePercent(success);
|
||||
|
||||
coverOptions.value.series.data = handlePassRatePercent(cover);
|
||||
completeOptions.value.series.data = handlePassRatePercent(success);
|
||||
|
||||
coverOptions.value.title.text = cover[0].name ?? '';
|
||||
coverOptions.value.title.subtext = `${cover[0].count ?? 0}%`;
|
||||
|
||||
completeOptions.value.title.text = success[0].name ?? '';
|
||||
completeOptions.value.title.subtext = `${success[0].count ?? 0}%`;
|
||||
|
||||
coverOptions.value.series.color = ['#00C261', '#D4D4D8'];
|
||||
completeOptions.value.series.color = ['#00C261', '#ED0303'];
|
||||
}
|
||||
|
||||
function initApiCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const params = {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
apiCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initApiCount();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initApiCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<a-button type="secondary" @click="exitHandler">
|
||||
{{ t('workbench.homePage.exitEdit') }}
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="confirmLoading" @click="saveHandler">
|
||||
<a-button type="primary" :disabled="!selectedCardList.length" :loading="confirmLoading" @click="saveHandler">
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
@ -39,9 +39,15 @@
|
|||
>
|
||||
<div class="card-item-text">
|
||||
<a-tooltip :content="t('workbench.homePage.sort')" :mouse-enter-delay="300">
|
||||
<MsIcon type="icon-icon_drag" size="16" class="cursor-move text-[var(--color-text-4)]" />
|
||||
<div class="flex hover:bg-[rgb(var(--primary-1))]">
|
||||
<MsIcon
|
||||
type="icon-icon_drag"
|
||||
size="16"
|
||||
class="cursor-move text-[var(--color-text-4)] hover:text-[rgb(var(--primary-7))]"
|
||||
/>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<div>{{ item.label }}</div>
|
||||
<div>{{ t(item.label) }}</div>
|
||||
</div>
|
||||
<div class="card-item-text">
|
||||
<a-radio-group
|
||||
|
@ -83,12 +89,14 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import NotData from '../../components/notData.vue';
|
||||
import CardSettingList from '@/views/workbench/homePage/components/cardSettingList.vue';
|
||||
|
||||
import { editDashboardLayout } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getGenerateId } from '@/utils';
|
||||
|
@ -100,6 +108,14 @@
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
list: SelectedCardItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const innerVisible = defineModel<boolean>('visible', {
|
||||
required: true,
|
||||
});
|
||||
|
@ -143,17 +159,36 @@
|
|||
// 保存
|
||||
async function saveHandler() {
|
||||
try {
|
||||
confirmLoading.value = true;
|
||||
selectedCardList.value = selectedCardList.value.map((e, pos) => {
|
||||
return {
|
||||
...e,
|
||||
pos,
|
||||
};
|
||||
});
|
||||
await editDashboardLayout(selectedCardList.value, appStore.currentOrgId);
|
||||
emit('success');
|
||||
innerVisible.value = false;
|
||||
Message.success(t('common.saveSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => innerVisible.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
selectedCardList.value = cloneDeep(props.list);
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
</div>
|
||||
|
||||
<div class="card-config-menu-wrapper">
|
||||
<a-menu class="w-full" :default-open-keys="defaultOpenKeys">
|
||||
<a-menu v-if="filteredConfigList.length" class="w-full" :default-open-keys="defaultOpenKeys">
|
||||
<a-sub-menu v-for="item of filteredConfigList" :key="item.value">
|
||||
<template #title>
|
||||
<div class="font-medium text-[var(--color-text-1)]">
|
||||
{{ item.label }}
|
||||
{{ t(item.label) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -25,15 +25,22 @@
|
|||
<svg-icon width="98px" height="69px" :name="ele.img" />
|
||||
</div>
|
||||
<div class="card-config-text">
|
||||
<div>{{ ele.label }}</div>
|
||||
<div>{{ t(ele.label) }}</div>
|
||||
<div class="card-config-desc flex">
|
||||
<div>{{ ele.description }}</div>
|
||||
<div>{{ t(ele.description || '') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
<div v-else class="p-[16px]">
|
||||
<div
|
||||
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
||||
>
|
||||
{{ t('common.noData') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -53,144 +60,149 @@
|
|||
}>();
|
||||
|
||||
const configList = ref<WorkConfigCard[]>([
|
||||
// 概览
|
||||
{
|
||||
label: t('workbench.homePage.overview'),
|
||||
label: 'workbench.homePage.overview',
|
||||
value: 'overview',
|
||||
description: '',
|
||||
img: '',
|
||||
children: [
|
||||
{
|
||||
label: t('workbench.homePage.projectOverview'),
|
||||
label: 'workbench.homePage.projectOverview',
|
||||
value: WorkCardEnum.PROJECT_VIEW,
|
||||
description: t('workbench.homePage.projectOverviewDesc'),
|
||||
description: 'workbench.homePage.projectOverviewDesc',
|
||||
img: 'project-overview-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.staffOverview'),
|
||||
label: 'workbench.homePage.staffOverview',
|
||||
value: WorkCardEnum.PROJECT_MEMBER_VIEW,
|
||||
description: t('workbench.homePage.staffOverviewDesc'),
|
||||
description: 'workbench.homePage.staffOverviewDesc',
|
||||
img: 'staff-overview-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.createdByMe'),
|
||||
label: 'workbench.homePage.createdByMe',
|
||||
value: WorkCardEnum.CREATE_BY_ME,
|
||||
description: t('workbench.homePage.createdByMeDesc'),
|
||||
description: 'workbench.homePage.createdByMeDesc',
|
||||
img: 'my-created-project-img',
|
||||
},
|
||||
],
|
||||
},
|
||||
// 测试用例
|
||||
{
|
||||
label: t('menu.caseManagement'),
|
||||
label: 'menu.caseManagement',
|
||||
value: 'caseManagement',
|
||||
description: '',
|
||||
img: '',
|
||||
children: [
|
||||
{
|
||||
label: t('workbench.homePage.useCasesNumber'),
|
||||
label: 'workbench.homePage.useCasesNumber',
|
||||
value: WorkCardEnum.CASE_COUNT,
|
||||
description: t('workbench.homePage.useCasesNumberDesc'),
|
||||
description: 'workbench.homePage.useCasesNumberDesc',
|
||||
img: 'link-case-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.useCasesCount'),
|
||||
label: 'workbench.homePage.useCasesCount',
|
||||
value: WorkCardEnum.ASSOCIATE_CASE_COUNT,
|
||||
description: t('workbench.homePage.useCasesCountDesc'),
|
||||
description: 'workbench.homePage.useCasesCountDesc',
|
||||
img: 'case-count-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.numberOfCaseReviews'),
|
||||
label: 'workbench.homePage.numberOfCaseReviews',
|
||||
value: WorkCardEnum.REVIEW_CASE_COUNT,
|
||||
description: t('workbench.homePage.numberOfCaseReviewsDesc'),
|
||||
description: 'workbench.homePage.numberOfCaseReviewsDesc',
|
||||
img: 'case-review-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.waitForReview'),
|
||||
label: 'workbench.homePage.waitForReview',
|
||||
value: WorkCardEnum.REVIEWING_BY_ME,
|
||||
description: t('workbench.homePage.waitForReviewDesc'),
|
||||
description: 'workbench.homePage.waitForReviewDesc',
|
||||
img: 'wait-review-img',
|
||||
},
|
||||
],
|
||||
},
|
||||
// 接口测试
|
||||
{
|
||||
label: t('menu.apiTest'),
|
||||
label: 'menu.apiTest',
|
||||
value: 'apiTest',
|
||||
description: '',
|
||||
img: '',
|
||||
children: [
|
||||
{
|
||||
label: t('workbench.homePage.apiCount'),
|
||||
label: 'workbench.homePage.apiCount',
|
||||
value: WorkCardEnum.API_COUNT,
|
||||
description: t('workbench.homePage.apiCountDesc'),
|
||||
description: 'workbench.homePage.apiCountDesc',
|
||||
img: 'api-count-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.apiUseCasesNumber'),
|
||||
label: 'workbench.homePage.apiUseCasesNumber',
|
||||
value: WorkCardEnum.API_CASE_COUNT,
|
||||
description: t('workbench.homePage.apiUseCasesNumberDesc'),
|
||||
description: 'workbench.homePage.apiUseCasesNumberDesc',
|
||||
img: 'api-use-case-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.scenarioUseCasesNumber'),
|
||||
label: 'workbench.homePage.scenarioUseCasesNumber',
|
||||
value: WorkCardEnum.SCENARIO_COUNT,
|
||||
description: t('workbench.homePage.scenarioUseCasesNumberDesc'),
|
||||
description: 'workbench.homePage.scenarioUseCasesNumberDesc',
|
||||
img: 'scenario-case-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceChange'),
|
||||
label: 'workbench.homePage.interfaceChange',
|
||||
value: WorkCardEnum.API_CHANGE,
|
||||
description: t('workbench.homePage.interfaceChangeDesc'),
|
||||
description: 'workbench.homePage.interfaceChangeDesc',
|
||||
img: 'api-change-img',
|
||||
},
|
||||
],
|
||||
},
|
||||
// 测试计划
|
||||
{
|
||||
label: t('menu.testPlan'),
|
||||
label: 'menu.testPlan',
|
||||
value: 'testPlan',
|
||||
description: '',
|
||||
img: '',
|
||||
children: [
|
||||
{
|
||||
label: t('workbench.homePage.numberOfTestPlan'),
|
||||
label: 'workbench.homePage.numberOfTestPlan',
|
||||
value: WorkCardEnum.TEST_PLAN_COUNT,
|
||||
description: t('workbench.homePage.numberOfTestPlanDesc'),
|
||||
description: 'workbench.homePage.numberOfTestPlanDesc',
|
||||
img: 'test-plan-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.remainingBugOfPlan'),
|
||||
label: 'workbench.homePage.remainingBugOfPlan',
|
||||
value: WorkCardEnum.PLAN_LEGACY_BUG,
|
||||
description: t('workbench.homePage.remainingBugOfPlanDesc'),
|
||||
description: 'workbench.homePage.remainingBugOfPlanDesc',
|
||||
img: 'test-plan-bug-img',
|
||||
},
|
||||
],
|
||||
},
|
||||
// 缺陷管理
|
||||
{
|
||||
label: t('menu.bugManagement'),
|
||||
label: 'menu.bugManagement',
|
||||
value: 'bugManagement',
|
||||
description: '',
|
||||
img: '',
|
||||
children: [
|
||||
{
|
||||
label: t('workbench.homePage.bugCount'),
|
||||
label: 'workbench.homePage.bugCount',
|
||||
value: WorkCardEnum.BUG_COUNT,
|
||||
description: t('workbench.homePage.bugCountDesc'),
|
||||
description: 'workbench.homePage.bugCountDesc',
|
||||
img: 'bug-count-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.createdBugByMe'),
|
||||
label: 'workbench.homePage.createdBugByMe',
|
||||
value: WorkCardEnum.CREATE_BUG_BY_ME,
|
||||
description: t('workbench.homePage.createdBugByMeDesc'),
|
||||
description: 'workbench.homePage.createdBugByMeDesc',
|
||||
img: 'my-created-bug-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.pendingDefect'),
|
||||
label: 'workbench.homePage.pendingDefect',
|
||||
value: WorkCardEnum.HANDLE_BUG_BY_ME,
|
||||
description: t('workbench.homePage.pendingDefectDesc'),
|
||||
description: 'workbench.homePage.pendingDefectDesc',
|
||||
img: 'wait-handle-bug-img',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.defectProcessingNumber'),
|
||||
label: 'workbench.homePage.defectProcessingNumber',
|
||||
value: WorkCardEnum.BUG_HANDLE_USER,
|
||||
description: t('workbench.homePage.defectProcessingNumberDesc'),
|
||||
description: 'workbench.homePage.defectProcessingNumberDesc',
|
||||
img: 'bug-handler-img',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.useCasesNumber') }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -17,17 +16,21 @@
|
|||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="reviewValueList" />
|
||||
</div>
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="passValueList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<SetReportChart size="120px" :legend-data="legendData" :options="executeCharOptions" :request-total="100000" />
|
||||
<TabCard :content-tab-list="caseCountTabList" not-has-padding hidden-border min-width="270px">
|
||||
<template #item="{ item: tabItem }">
|
||||
<div class="w-full">
|
||||
<PassRatePie
|
||||
:options="tabItem.options"
|
||||
:tooltip-text="tabItem.tooltip"
|
||||
:size="60"
|
||||
:value-list="tabItem.valueList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TabCard>
|
||||
<div class="h-[148px]">
|
||||
<MsChart :options="caseCountOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,53 +41,56 @@
|
|||
* @desc 用例数量
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
import TabCard from './tabCard.vue';
|
||||
|
||||
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { workCaseCountDetail } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
import { StatusStatisticsMapType } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
|
||||
const projectIds = ref('');
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const options = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
const options = ref(cloneDeep(commonRatePieOptions));
|
||||
|
||||
function handlePassRatePercent(data: { name: string; count: number }[]) {
|
||||
return data.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const reviewValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.reviewed'),
|
||||
|
@ -95,6 +101,7 @@
|
|||
value: 2000,
|
||||
},
|
||||
]);
|
||||
|
||||
const passValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.havePassed'),
|
||||
|
@ -106,101 +113,104 @@
|
|||
},
|
||||
]);
|
||||
|
||||
const legendData = ref<LegendData[]>([
|
||||
const reviewOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const passOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const caseCountTabList = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'P0',
|
||||
value: 'P0',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--danger-6))] ml-[24px]',
|
||||
label: '',
|
||||
value: 'execution',
|
||||
valueList: reviewValueList.value,
|
||||
options: { ...reviewOptions.value },
|
||||
tooltip: 'workbench.homePage.reviewRateTooltip',
|
||||
},
|
||||
{
|
||||
label: 'P1',
|
||||
value: 'P1',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--warning-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: 'P2',
|
||||
value: 'P2',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: 'P3',
|
||||
value: 'P3',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
|
||||
},
|
||||
]);
|
||||
|
||||
// 执行分析
|
||||
const executeCharOptions = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
series: {
|
||||
...seriesConfig,
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
label: '',
|
||||
value: 'pass',
|
||||
valueList: passValueList.value,
|
||||
options: { ...passOptions.value },
|
||||
tooltip: 'workbench.homePage.reviewPassRateTooltip',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// 处理X率饼图数据
|
||||
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
|
||||
const { review, pass } = statusStatisticsMap;
|
||||
reviewValueList.value = handlePassRatePercent(review);
|
||||
passValueList.value = handlePassRatePercent(pass);
|
||||
|
||||
reviewOptions.value.series.data = handlePassRatePercent(review);
|
||||
passOptions.value.series.data = handlePassRatePercent(pass);
|
||||
|
||||
reviewOptions.value.title.text = review[0].name ?? '';
|
||||
reviewOptions.value.title.subtext = `${review[0].count ?? 0}%`;
|
||||
passOptions.value.title.text = pass[0].name ?? '';
|
||||
passOptions.value.title.subtext = `${pass[0].count ?? 0}%`;
|
||||
reviewOptions.value.series.color = ['#00C261', '#D4D4D8'];
|
||||
passOptions.value.series.color = ['#00C261', '#ED0303'];
|
||||
}
|
||||
|
||||
const caseCountOptions = ref<Record<string, any>>({});
|
||||
async function initCaseCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const params = {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const detail = await workCaseCountDetail(params);
|
||||
const { statusStatisticsMap, statusPercentList } = detail;
|
||||
caseCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCaseCount();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initCaseCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initCaseCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initCaseCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-wrapper {
|
||||
margin: 16px 0;
|
||||
padding: 24px;
|
||||
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
|
||||
@apply rounded-xl bg-white;
|
||||
.title {
|
||||
font-size: 16px;
|
||||
@apply font-medium;
|
||||
}
|
||||
.case-count-wrapper {
|
||||
@apply flex items-center gap-4;
|
||||
.case-count-item {
|
||||
@apply flex-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="less"></style>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.numberOfCaseReviews') }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -17,14 +16,19 @@
|
|||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="coverValueList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<SetReportChart size="120px" :legend-data="legendData" :options="executeCharOptions" :request-total="100000" />
|
||||
<div class="case-count-wrapper mb-[16px]">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie
|
||||
:options="options"
|
||||
tooltip-text="workbench.homePage.caseReviewCoverRateTooltip"
|
||||
:size="60"
|
||||
:value-list="coverValueList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[148px]">
|
||||
<MsChart :options="caseReviewCountOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,53 +39,43 @@
|
|||
* @desc 用例评审数量
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
|
||||
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const projectIds = ref('');
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const options = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
const options = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
|
||||
|
||||
const coverValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.covered'),
|
||||
|
@ -93,83 +87,79 @@
|
|||
},
|
||||
]);
|
||||
|
||||
const legendData = ref<LegendData[]>([
|
||||
{
|
||||
label: t('common.notStarted'),
|
||||
value: 'P0',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--primary-4))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.inProgress'),
|
||||
value: 'P1',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.completed'),
|
||||
value: 'P2',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.archived'),
|
||||
value: 'P3',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
|
||||
},
|
||||
]);
|
||||
|
||||
const executeCharOptions = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
series: {
|
||||
...seriesConfig,
|
||||
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',
|
||||
},
|
||||
},
|
||||
// TODO 假数据
|
||||
const detail = ref<PassRateDataType>({
|
||||
statusStatisticsMap: {
|
||||
cover: [
|
||||
{ name: '覆盖率', count: 10 },
|
||||
{ name: '已覆盖', count: 2 },
|
||||
{ name: '未覆盖', count: 1 },
|
||||
],
|
||||
},
|
||||
statusPercentList: [
|
||||
{ status: '未开始', count: 1, percentValue: '10%' },
|
||||
{ status: '进行中', count: 3, percentValue: '0%' },
|
||||
{ status: '已完成', count: 6, percentValue: '0%' },
|
||||
{ status: '已归档', count: 7, percentValue: '0%' },
|
||||
],
|
||||
});
|
||||
|
||||
const caseReviewCountOptions = ref<Record<string, any>>({});
|
||||
function initApiCount() {
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
caseReviewCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
const { cover } = statusStatisticsMap;
|
||||
|
||||
coverValueList.value = cover.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
|
||||
options.value.series.data = coverValueList.value;
|
||||
options.value.title.text = cover[0].name ?? '';
|
||||
options.value.title.subtext = `${cover[0].count ?? 0}%`;
|
||||
options.value.series.color = ['#00C261', '#D4D4D8'];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initApiCount();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initApiCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ props.title }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="props.valueList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<SetReportChart
|
||||
size="120px"
|
||||
:legend-data="props.legendData"
|
||||
:options="executeCharOptions"
|
||||
:request-total="100000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** *
|
||||
* @desc 用于缺陷数量,待我处理的缺陷数量组件
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
|
||||
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const projectIds = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
valueList: {
|
||||
label: string;
|
||||
value: number;
|
||||
}[];
|
||||
title: string;
|
||||
legendData: LegendData[];
|
||||
}>();
|
||||
|
||||
const options = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
|
||||
const executeCharOptions = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
series: {
|
||||
...seriesConfig,
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
|
@ -1,38 +1,72 @@
|
|||
<template>
|
||||
<CountOverview :value-list="valueList" :legend-data="legendData" :title="title" />
|
||||
<div class="card-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t(props.item.label) }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item mb-[16px]">
|
||||
<PassRatePie :tooltip-text="tooltip" :options="legacyOptions" :size="60" :value-list="valueList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[148px]">
|
||||
<MsChart :options="countOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** *
|
||||
* @desc 待我处理的缺陷&缺陷数量
|
||||
* @desc 用于缺陷数量,待我处理的缺陷数量组件
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import CountOverview from './countOverview.vue';
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type {
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
StatusStatisticsMapType,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
type: WorkCardEnum;
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const title = computed(() => {
|
||||
switch (props.type) {
|
||||
case WorkCardEnum.HANDLE_BUG_BY_ME:
|
||||
return t('workbench.homePage.pendingDefect');
|
||||
case WorkCardEnum.CREATE_BUG_BY_ME:
|
||||
return t('workbench.homePage.createdBugByMe');
|
||||
case WorkCardEnum.PLAN_LEGACY_BUG:
|
||||
return t('workbench.homePage.remainingBugOfPlan');
|
||||
default:
|
||||
return t('workbench.homePage.bugCount');
|
||||
}
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const valueList = ref<
|
||||
{
|
||||
label: string;
|
||||
|
@ -49,36 +83,113 @@
|
|||
},
|
||||
]);
|
||||
|
||||
const legendData = ref<LegendData[]>([
|
||||
{
|
||||
label: t('common.notStarted'),
|
||||
value: 'notStarted',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--primary-4))] ml-[24px]',
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
const legacyOptions = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
|
||||
|
||||
// TODO 假数据
|
||||
const detail = ref<PassRateDataType>({
|
||||
statusStatisticsMap: {
|
||||
legacy: [
|
||||
{ name: '遗留率', count: 10 },
|
||||
{ name: '缺陷总数', count: 2 },
|
||||
{ name: '遗留缺陷数', count: 1 },
|
||||
],
|
||||
},
|
||||
statusPercentList: [
|
||||
{ status: 'AAA', count: 1, percentValue: '10%' },
|
||||
{ status: 'BBB', count: 3, percentValue: '0%' },
|
||||
{ status: 'CCC', count: 6, percentValue: '0%' },
|
||||
],
|
||||
});
|
||||
|
||||
const countOptions = ref({});
|
||||
|
||||
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
|
||||
const { legacy } = statusStatisticsMap;
|
||||
valueList.value = legacy.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
legacyOptions.value.series.data = valueList.value;
|
||||
|
||||
legacyOptions.value.title.text = legacy[0].name ?? '';
|
||||
legacyOptions.value.title.subtext = `${legacy[0].count ?? 0}%`;
|
||||
legacyOptions.value.series.color = ['#D4D4D8', '#00C261'];
|
||||
}
|
||||
|
||||
async function initCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const params = {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
countOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const tooltip = computed(() => {
|
||||
return props.item.key === WorkCardEnum.PLAN_LEGACY_BUG ? 'workbench.homePage.planCaseCountLegacyRateTooltip' : '';
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initCount();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.inProgress'),
|
||||
value: 'inProgress',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.completed'),
|
||||
value: 'completed',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.archived'),
|
||||
value: 'archived',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
|
||||
},
|
||||
]);
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.defectProcessingNumber') }} </div>
|
||||
<div class="title"> {{ t(props.item.label) }} </div>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<MsChart height="300px" :options="options" />
|
||||
<MsChart height="260px" :options="options" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -47,14 +47,38 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { defectStatusColor, getCommonBarOptions } from '../utils';
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonColorConfig, getCommonBarOptions } from '../utils';
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const memberIds = ref('');
|
||||
const projectIds = ref('');
|
||||
const appStore = useAppStore();
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = computed<string>({
|
||||
get: () => {
|
||||
const [newProject] = innerProjectIds.value;
|
||||
return newProject;
|
||||
},
|
||||
set: (val) => val,
|
||||
});
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
const memberOptions = ref<SelectOptionData[]>([]);
|
||||
|
||||
|
@ -63,30 +87,139 @@
|
|||
const hasRoom = computed(() => members.value.length >= 7);
|
||||
const seriesData = ref<Record<string, any>[]>([
|
||||
{
|
||||
name: '已结束',
|
||||
name: '新创建',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0],
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [400, 200, 400, 200, 400, 200],
|
||||
},
|
||||
{
|
||||
name: '未结束',
|
||||
name: '激活',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '处理中',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '已关闭',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '新创建1',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [400, 200, 400, 200, 400, 200],
|
||||
},
|
||||
{
|
||||
name: '激活1',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '处理中1',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '已关闭1',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '已关闭2',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
{
|
||||
name: '已关闭3',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [90, 160, 90, 160, 90, 160],
|
||||
},
|
||||
]);
|
||||
const defectStatusColor = ['#811FA3', '#FFA200', '#3370FF', '#F24F4F'];
|
||||
|
||||
onMounted(() => {
|
||||
options.value = getCommonBarOptions(hasRoom.value, defectStatusColor);
|
||||
function getDefectMemberDetail() {
|
||||
options.value = getCommonBarOptions(hasRoom.value, [...defectStatusColor, ...commonColorConfig]);
|
||||
options.value.xAxis.data = members.value;
|
||||
options.value.series = seriesData.value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDefectMemberDetail();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
getDefectMemberDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getDefectMemberDetail();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ props.title }} </div>
|
||||
<div class="title"> {{ t(props.item.label) }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="innerProjectIds"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
|
@ -15,13 +15,14 @@
|
|||
:prefix="t('workbench.homePage.project')"
|
||||
:multiple="true"
|
||||
:has-all-select="true"
|
||||
:default-all-select="true"
|
||||
:default-all-select="!(props.item.projectIds || []).length"
|
||||
:at-least-one="true"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<TabCard :content-tab-list="contentTabList" />
|
||||
<TabCard :content-tab-list="cardModuleList" />
|
||||
</div>
|
||||
<!-- 概览图 -->
|
||||
<div>
|
||||
|
@ -40,152 +41,136 @@
|
|||
import MsSelect from '@/components/business/ms-select';
|
||||
import TabCard from './tabCard.vue';
|
||||
|
||||
import { workMyCreatedDetail, workProOverviewDetail } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
|
||||
import type {
|
||||
ModuleCardItem,
|
||||
OverViewOfProject,
|
||||
SelectedCardItem,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum, WorkOverviewEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
import { commonColorConfig, getCommonBarOptions } from '../utils';
|
||||
import { commonColorConfig, contentTabList, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const projectIds = ref('');
|
||||
|
||||
const contentTabList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.functionalUseCase'),
|
||||
value: WorkOverviewEnum.FUNCTIONAL,
|
||||
icon: WorkOverviewIconEnum.FUNCTIONAL,
|
||||
color: 'rgb(var(--primary-5))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.useCaseReview'),
|
||||
value: WorkOverviewEnum.CASE_REVIEW,
|
||||
icon: WorkOverviewIconEnum.CASE_REVIEW,
|
||||
color: 'rgb(var(--success-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceAPI'),
|
||||
value: WorkOverviewEnum.API,
|
||||
icon: WorkOverviewIconEnum.API,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceCASE'),
|
||||
value: WorkOverviewEnum.API_CASE,
|
||||
icon: WorkOverviewIconEnum.API_CASE,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceScenario'),
|
||||
value: WorkOverviewEnum.API_SCENARIO,
|
||||
icon: WorkOverviewIconEnum.API_SCENARIO,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.apiPlan'),
|
||||
value: WorkOverviewEnum.TEST_PLAN,
|
||||
icon: WorkOverviewIconEnum.TEST_PLAN,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.bugCount'),
|
||||
value: WorkOverviewEnum.BUG_COUNT,
|
||||
icon: WorkOverviewIconEnum.BUG_COUNT,
|
||||
color: 'rgb(var(--danger-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
]);
|
||||
|
||||
const xAxisType = computed(() => {
|
||||
return contentTabList.value.map((e) => e.label);
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const hasRoom = computed(() => projectIds.value.length >= 7);
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.item.projectIds,
|
||||
(val) => {
|
||||
innerProjectIds.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
const hasRoom = computed(() => innerProjectIds.value.length >= 7);
|
||||
|
||||
const options = ref<Record<string, any>>({});
|
||||
const seriesData = ref<Record<string, any>[]>([
|
||||
{
|
||||
name: '项目A',
|
||||
|
||||
const cardModuleList = ref<ModuleCardItem[]>([]);
|
||||
|
||||
function handleData(detail: OverViewOfProject) {
|
||||
// 处理模块顺序
|
||||
const tempAxisData = detail.xaxis.map((xAxisKey) => {
|
||||
const data = contentTabList.value.find((e) => e.value === xAxisKey);
|
||||
return {
|
||||
...data,
|
||||
count: detail.caseCountMap[xAxisKey as WorkOverviewEnum],
|
||||
};
|
||||
});
|
||||
|
||||
cardModuleList.value = tempAxisData as ModuleCardItem[];
|
||||
options.value = getCommonBarOptions(hasRoom.value, commonColorConfig);
|
||||
const { invisible, text } = handleNoDataDisplay(detail.xaxis, detail.projectCountList);
|
||||
options.value.graphic.invisible = invisible;
|
||||
options.value.graphic.style.text = text;
|
||||
// x轴
|
||||
options.value.xAxis.data = cardModuleList.value.map((e) => e.label);
|
||||
|
||||
// 处理data数据
|
||||
options.value.series = detail.projectCountList.map((item) => {
|
||||
return {
|
||||
name: item.name,
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [null, 230, 150, 80, 70, 110, 130],
|
||||
},
|
||||
{
|
||||
name: '项目B',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [90, 160, 130, 100, 90, 120, 140],
|
||||
},
|
||||
{
|
||||
name: '项目C',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [100, 140, 120, 90, 100, 130, 120],
|
||||
},
|
||||
{
|
||||
name: '项目D',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [90, 160, 130, 100, 90, 120, 140],
|
||||
},
|
||||
{
|
||||
name: '项目E',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [100, 140, 120, 90, 100, 130, 120],
|
||||
},
|
||||
{
|
||||
name: '项目F',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [100, 140, 120, 90, 40, 130, 120],
|
||||
},
|
||||
{
|
||||
name: '项目G',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
},
|
||||
data: [100, 140, 120, 90, 40, 130, 120],
|
||||
},
|
||||
]);
|
||||
data: item.count,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function initOverViewDetail() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const params = {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
let detail;
|
||||
if (props.item.key === WorkCardEnum.PROJECT_VIEW) {
|
||||
detail = await workProOverviewDetail(params);
|
||||
} else {
|
||||
detail = await workMyCreatedDetail(params);
|
||||
}
|
||||
|
||||
handleData(detail);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
options.value = getCommonBarOptions(hasRoom.value, commonColorConfig);
|
||||
options.value.xAxis.data = xAxisType.value;
|
||||
options.value.series = seriesData.value;
|
||||
initOverViewDetail();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initOverViewDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initOverViewDetail();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.staffOverview') }} </div>
|
||||
<div class="title"> {{ t(props.item.label) }} </div>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -18,8 +17,10 @@
|
|||
<MsSelect
|
||||
v-model:model-value="memberIds"
|
||||
:options="memberOptions"
|
||||
:allow-search="false"
|
||||
allow-search
|
||||
allow-clear
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.staff')"
|
||||
:multiple="true"
|
||||
|
@ -29,11 +30,8 @@
|
|||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<TabCard :content-tab-list="contentTabList" />
|
||||
</div>
|
||||
<!-- 概览图 -->
|
||||
<div>
|
||||
<div class="mt-[16px]">
|
||||
<MsChart height="300px" :options="options" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,233 +45,142 @@
|
|||
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import TabCard from './tabCard.vue';
|
||||
|
||||
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
|
||||
import { workMemberViewDetail } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
import { characterLimit } from '@/utils';
|
||||
|
||||
import { WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
|
||||
import type { OverViewOfProject, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonColorConfig } from '../utils';
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import { commonColorConfig, contentTabList, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const memberIds = ref('');
|
||||
const projectIds = ref('');
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const memberOptions = ref<SelectOptionData[]>([]);
|
||||
const innerHandleUsers = defineModel<string[]>('handleUsers', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const contentTabList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.functionalUseCase'),
|
||||
value: WorkOverviewEnum.FUNCTIONAL,
|
||||
icon: WorkOverviewIconEnum.FUNCTIONAL,
|
||||
color: 'rgb(var(--primary-5))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.useCaseReview'),
|
||||
value: WorkOverviewEnum.CASE_REVIEW,
|
||||
icon: WorkOverviewIconEnum.CASE_REVIEW,
|
||||
color: 'rgb(var(--success-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceAPI'),
|
||||
value: WorkOverviewEnum.API,
|
||||
icon: WorkOverviewIconEnum.API,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceCASE'),
|
||||
value: WorkOverviewEnum.API_CASE,
|
||||
icon: WorkOverviewIconEnum.API_CASE,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceScenario'),
|
||||
value: WorkOverviewEnum.API_SCENARIO,
|
||||
icon: WorkOverviewIconEnum.API_SCENARIO,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.apiPlan'),
|
||||
value: WorkOverviewEnum.TEST_PLAN,
|
||||
icon: WorkOverviewIconEnum.TEST_PLAN,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.bugCount'),
|
||||
value: WorkOverviewEnum.BUG_COUNT,
|
||||
icon: WorkOverviewIconEnum.BUG_COUNT,
|
||||
color: 'rgb(var(--danger-6))',
|
||||
count: 1000000,
|
||||
},
|
||||
]);
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const hasRoom = computed(() => projectIds.value.length >= 7);
|
||||
// 静态数据 TODO
|
||||
const members = computed(() => ['张三', '李四', '王五', '小王']);
|
||||
const memberIds = ref<string[]>(innerHandleUsers.value);
|
||||
|
||||
const staticData = [
|
||||
{
|
||||
name: '',
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
const hasRoom = computed(() => memberIds.value.length >= 7);
|
||||
const memberOptions = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
const options = ref<Record<string, any>>({});
|
||||
function handleData(detail: OverViewOfProject) {
|
||||
options.value = getCommonBarOptions(hasRoom.value, commonColorConfig);
|
||||
const { invisible, text } = handleNoDataDisplay(detail.xaxis, detail.projectCountList);
|
||||
options.value.graphic.invisible = invisible;
|
||||
options.value.graphic.style.text = text;
|
||||
options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10));
|
||||
options.value.series = detail.projectCountList.map((item, index) => {
|
||||
return {
|
||||
name: contentTabList.value[index].label,
|
||||
type: 'bar',
|
||||
stack: 'member',
|
||||
barWidth: 12,
|
||||
data: [400, 200, 150, 80, 70, 110, 130],
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
stack: 'member',
|
||||
barWidth: 12,
|
||||
data: [90, 160, 130, 100, 90, 120, 140],
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
stack: 'member',
|
||||
barWidth: 12,
|
||||
data: [100, 140, 120, 90, 100, 130, 120],
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
stack: 'member',
|
||||
barWidth: 12,
|
||||
data: [90, 160, 130, 100, 90, 120, 140],
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'member',
|
||||
data: [100, 140, 120, 90, 100, 130, 120],
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'member',
|
||||
data: [100, 140, 120, 90, 40, 130, 120],
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
stack: 'member',
|
||||
barWidth: 12,
|
||||
data: [100, 140, 120, 90, 40, 130, 120],
|
||||
data: item.count,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0], // 上边圆角
|
||||
borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const lastSeriousData = computed(() => {
|
||||
contentTabList.value.forEach((e, i) => {
|
||||
staticData[i].name = e.label;
|
||||
});
|
||||
return staticData;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const options = ref({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
borderWidth: 0,
|
||||
formatter(params: any) {
|
||||
const html = `
|
||||
<div class="w-[186px] h-[auto] p-[16px] flex flex-col">
|
||||
${params
|
||||
.map(
|
||||
(item: any) => `
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm" style="background:${item.color}"></div>
|
||||
<div style="color:#959598">${item.seriesName}</div>
|
||||
</div>
|
||||
<div class="text-[#323233] font-medium">${addCommasToNumber(item.value)}</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
return html;
|
||||
},
|
||||
},
|
||||
color: commonColorConfig,
|
||||
grid: {
|
||||
top: '36px',
|
||||
left: '10px',
|
||||
right: '10px',
|
||||
bottom: hasRoom.value ? '54px' : '5px',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
splitLine: false,
|
||||
boundaryGap: true,
|
||||
type: 'category',
|
||||
data: members.value,
|
||||
axisLabel: {
|
||||
color: '#646466',
|
||||
},
|
||||
axisTick: {
|
||||
show: false, // 隐藏刻度线
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#EDEDF1',
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '单位:个', // 设置单位
|
||||
nameLocation: 'end',
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#AEAEB2', // 自定义字体大小和颜色
|
||||
padding: [0, 0, 0, -20], // 通过左侧(最后一个值)的负偏移向左移动
|
||||
},
|
||||
nameGap: 20,
|
||||
splitLine: {
|
||||
show: true, // 控制是否显示水平线
|
||||
lineStyle: {
|
||||
color: '#EDEDF1', // 水平线颜色
|
||||
width: 1, // 水平线宽度
|
||||
type: 'dashed', // 水平线线型,可选 'solid'、'dashed'、'dotted'
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
colorBy: 'series',
|
||||
series: lastSeriousData,
|
||||
legend: {
|
||||
show: true,
|
||||
type: 'scroll',
|
||||
itemGap: 20,
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
},
|
||||
dataZoom: hasRoom.value
|
||||
? [
|
||||
{
|
||||
type: 'inside',
|
||||
async function initOverViewMemberDetail() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const params = {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: innerHandleUsers.value,
|
||||
};
|
||||
const detail = await workMemberViewDetail(params);
|
||||
handleData(detail);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMemberOptions() {
|
||||
const [newProjectId] = innerProjectIds.value;
|
||||
const res = await getProjectOptions(newProjectId);
|
||||
memberOptions.value = res.map((e: any) => ({
|
||||
label: e.name,
|
||||
value: e.id,
|
||||
}));
|
||||
}
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
memberIds.value = [];
|
||||
getMemberOptions();
|
||||
initOverViewMemberDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => memberIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerHandleUsers.value = val;
|
||||
initOverViewMemberDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initOverViewMemberDetail();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getMemberOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
<template>
|
||||
<div class="pass-rate-content">
|
||||
<MsChart :height="`${props.size}px`" :width="`${props.size}px`" :options="options" />
|
||||
<div class="relative flex items-center justify-center">
|
||||
<a-tooltip
|
||||
v-if="props.tooltipText"
|
||||
:mouse-enter-delay="500"
|
||||
:content="t(props.tooltipText || '')"
|
||||
position="bottom"
|
||||
>
|
||||
<div class="tooltip-rate h-[50px] w-[50px]"></div>
|
||||
</a-tooltip>
|
||||
<MsChart :height="`${props.size}px`" :width="`${props.size}px`" :options="props.options" />
|
||||
</div>
|
||||
<div class="pass-rate-title flex-1">
|
||||
<div v-for="item of props.valueList" :key="item.label" class="flex-1">
|
||||
<div class="mb-[8px] text-[var(--color-text-4)]">{{ item.label }}</div>
|
||||
|
@ -23,6 +33,7 @@
|
|||
const props = defineProps<{
|
||||
options: Record<string, any>;
|
||||
size: number;
|
||||
tooltipText?: string;
|
||||
valueList: {
|
||||
label: string;
|
||||
value: number;
|
||||
|
@ -46,4 +57,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.tooltip-rate {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
data: Record<string, any>[];
|
||||
data: { name: string; value: number }[];
|
||||
rateConfig: {
|
||||
name: string;
|
||||
count: string;
|
||||
color: string[];
|
||||
};
|
||||
}>();
|
||||
|
||||
|
@ -25,7 +26,7 @@
|
|||
show: true,
|
||||
text: '',
|
||||
left: 'center',
|
||||
top: '24px',
|
||||
top: 32,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
|
@ -36,6 +37,7 @@
|
|||
fontSize: 14,
|
||||
color: '#323233',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 3,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
|
@ -80,8 +82,10 @@
|
|||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
padAngle: 1,
|
||||
radius: ['46%', '56%'],
|
||||
center: ['50%', '32%'],
|
||||
color: [],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
|
@ -103,16 +107,18 @@
|
|||
const options = ref({});
|
||||
|
||||
function initOptions() {
|
||||
const { name, count, color } = props.rateConfig;
|
||||
options.value = {
|
||||
...commonOptionConfig.value,
|
||||
title: {
|
||||
...commonOptionConfig.value.title,
|
||||
text: props.rateConfig.name,
|
||||
subtext: props.rateConfig.count,
|
||||
text: name,
|
||||
subtext: count,
|
||||
},
|
||||
series: {
|
||||
...commonOptionConfig.value.series,
|
||||
data: [...props.data],
|
||||
color,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div :class="`card-wrapper ${props.item.fullScreen ? '' : 'card-min-height'}`">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.useCasesCount') }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:search-keys="['name']"
|
||||
|
@ -17,23 +15,19 @@
|
|||
</MsSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-[16px]">
|
||||
<div class="mt-[16px]">
|
||||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie :options="options" :size="60" :value-list="coverRateValueList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<SetReportChart
|
||||
size="120px"
|
||||
gap="24"
|
||||
:legend-data="legendData"
|
||||
:options="executeCharOptions"
|
||||
:request-total="100000"
|
||||
<PassRatePie
|
||||
:options="options"
|
||||
tooltip-text="workbench.homePage.associateCaseCoverRateTooltip"
|
||||
:size="60"
|
||||
:value-list="coverRateValueList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -41,128 +35,112 @@
|
|||
* @desc 关联用例数量
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
|
||||
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { workAssociateCaseDetail } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions } from '../utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const projectIds = ref('');
|
||||
|
||||
const options = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const coverRateValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.covered'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.notCover'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const legendData = ref<LegendData[]>([
|
||||
{
|
||||
label: t('workbench.homePage.apiUseCases'),
|
||||
value: 'apiUseCases',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.sceneUseCase'),
|
||||
value: 'sceneUseCase',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
|
||||
},
|
||||
]);
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
// 执行分析
|
||||
const executeCharOptions = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
series: {
|
||||
...seriesConfig,
|
||||
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 options = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
|
||||
|
||||
const coverRateValueList = ref<{ value: number; label: string; name: string }[]>([]);
|
||||
|
||||
async function getRelatedCaseCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const detail = await workAssociateCaseDetail({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
});
|
||||
const { cover } = detail.statusStatisticsMap;
|
||||
coverRateValueList.value = cover.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
options.value.series.data = coverRateValueList.value;
|
||||
options.value.title.text = cover[0].name ?? '';
|
||||
options.value.title.subtext = `${cover[0].count ?? 0}%`;
|
||||
options.value.series.color = ['#00C261', '#D4D4D8'];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRelatedCaseCount();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
getRelatedCaseCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getRelatedCaseCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div ref="cardWrapperRef">
|
||||
<a-tabs v-if="props.contentTabList.length" default-active-key="1" class="ms-tab-card">
|
||||
<a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="`${item.label}`">
|
||||
<template #title>
|
||||
|
@ -45,23 +45,28 @@
|
|||
|
||||
const width = ref<string | number>();
|
||||
|
||||
const cardWrapperRef = ref<HTMLElement | null>(null);
|
||||
const calculateWidth = debounce(() => {
|
||||
const wrapperContent = document.querySelector('.card-wrapper') as HTMLElement;
|
||||
const wrapperContent = cardWrapperRef.value as HTMLElement;
|
||||
if (wrapperContent) {
|
||||
const wrapperTotalWidth = wrapperContent.offsetWidth;
|
||||
const gap = 16;
|
||||
const paddingNumber = 16;
|
||||
const paddingWidth = props.notHasPadding ? 0 : paddingNumber * 2; // 两边的内边距总和 (16px * 2)
|
||||
const gapWidth = (props.contentTabList.length - 1) * gap; // 总间隙宽度
|
||||
const borderWidth = props.hiddenBorder ? 0 : 2;
|
||||
const itemWidth = Math.floor((wrapperTotalWidth - paddingWidth - gapWidth) / props.contentTabList.length);
|
||||
width.value = `${itemWidth - borderWidth}px`;
|
||||
const itemWidth = Math.floor((wrapperTotalWidth - gapWidth) / props.contentTabList.length);
|
||||
width.value = `${itemWidth}px`;
|
||||
}
|
||||
}, 300);
|
||||
}, 50);
|
||||
|
||||
let resizeObserver: ResizeObserver;
|
||||
|
||||
onMounted(() => {
|
||||
const wrapperContent = cardWrapperRef.value;
|
||||
if (wrapperContent) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
calculateWidth();
|
||||
window.addEventListener('resize', calculateWidth);
|
||||
});
|
||||
resizeObserver.observe(wrapperContent);
|
||||
}
|
||||
});
|
||||
|
||||
const minwidth = ref();
|
||||
|
@ -74,12 +79,22 @@
|
|||
minwidth.value = `${newMinWidth || '136px'}`;
|
||||
padding.value = `${noPadding ? '0px' : '16px'}`;
|
||||
color.value = `${isHiddenBorder ? 'transparent' : 'var(--color-text-n8)'}`;
|
||||
calculateWidth();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.contentTabList,
|
||||
(val) => {
|
||||
if (val.length) {
|
||||
calculateWidth();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', calculateWidth);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-wrapper card-min-height">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title">
|
||||
{{ t('workbench.homePage.numberOfTestPlan') }}
|
||||
</div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -20,15 +19,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px]">
|
||||
<TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border>
|
||||
<template #item="{ item }">
|
||||
<TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border min-width="270px">
|
||||
<template #item="{ item: tabItem }">
|
||||
<div class="w-full">
|
||||
<PassRatePie :options="options" :size="60" :value-list="item.valueList" />
|
||||
<PassRatePie :options="tabItem.options" :size="60" :value-list="tabItem.valueList" />
|
||||
</div>
|
||||
</template>
|
||||
</TabCard>
|
||||
<div class="mt-[16px]">
|
||||
<SetReportChart size="120px" :legend-data="legendData" :options="testPlanCharOptions" :request-total="100000" />
|
||||
<div class="h-[148px]">
|
||||
<MsChart :options="testPlanCountOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,54 +38,78 @@
|
|||
* @desc 测试计划数量
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
import TabCard from './tabCard.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
|
||||
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import { WorkCardEnum } from '@/enums/workbenchEnum';
|
||||
import type {
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
StatusStatisticsMapType,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const projectIds = ref('');
|
||||
|
||||
const options = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
|
||||
// TODO 假数据后边几个要用
|
||||
const detail = ref<PassRateDataType>({
|
||||
statusStatisticsMap: {
|
||||
execute: [
|
||||
{ name: '覆盖率', count: 10 },
|
||||
{ name: '已覆盖', count: 2 },
|
||||
{ name: '未覆盖', count: 1 },
|
||||
],
|
||||
pass: [
|
||||
{ name: '覆盖率', count: 10 },
|
||||
{ name: '已覆盖', count: 2 },
|
||||
{ name: '未覆盖', count: 1 },
|
||||
],
|
||||
complete: [
|
||||
{ name: '覆盖率', count: 10 },
|
||||
{ name: '已覆盖', count: 2 },
|
||||
{ name: '未覆盖', count: 1 },
|
||||
],
|
||||
},
|
||||
statusPercentList: [
|
||||
{ status: 'HTTP', count: 1, percentValue: '10%' },
|
||||
{ status: 'TCP', count: 3, percentValue: '0%' },
|
||||
{ status: 'BBB', count: 6, percentValue: '0%' },
|
||||
],
|
||||
});
|
||||
|
||||
const options = ref(cloneDeep(commonRatePieOptions));
|
||||
const executionOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const passOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const completeOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
|
||||
// 执行率
|
||||
const executionValueList = ref([
|
||||
{
|
||||
|
@ -125,106 +148,127 @@
|
|||
value: 2000,
|
||||
},
|
||||
]);
|
||||
|
||||
const testPlanTabList = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: '',
|
||||
value: 'execution',
|
||||
valueList: executionValueList.value,
|
||||
options,
|
||||
options: { ...executionOptions.value },
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
value: 'pass',
|
||||
valueList: passValueList.value,
|
||||
options,
|
||||
options: { ...passOptions.value },
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
value: 'complete',
|
||||
valueList: completeValueList.value,
|
||||
options,
|
||||
options: { ...completeOptions.value },
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const testPlanCharOptions = ref({
|
||||
...commonConfig,
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
},
|
||||
series: {
|
||||
...seriesConfig,
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
function handlePassRatePercent(data: { name: string; count: number }[]) {
|
||||
return data.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
|
||||
const { execute, pass, complete } = statusStatisticsMap;
|
||||
executionValueList.value = handlePassRatePercent(execute);
|
||||
passValueList.value = handlePassRatePercent(pass);
|
||||
completeValueList.value = handlePassRatePercent(complete);
|
||||
|
||||
executionOptions.value.series.data = handlePassRatePercent(execute);
|
||||
passOptions.value.series.data = handlePassRatePercent(pass);
|
||||
completeOptions.value.series.data = handlePassRatePercent(complete);
|
||||
|
||||
executionOptions.value.title.text = execute[0].name ?? '';
|
||||
executionOptions.value.title.subtext = `${execute[0].count ?? 0}%`;
|
||||
|
||||
passOptions.value.title.text = pass[0].name ?? '';
|
||||
passOptions.value.title.subtext = `${pass[0].count ?? 0}%`;
|
||||
|
||||
completeOptions.value.title.text = complete[0].name ?? '';
|
||||
completeOptions.value.title.subtext = `${complete[0].count ?? 0}%`;
|
||||
|
||||
executionOptions.value.series.color = ['#D4D4D8', '#00C261'];
|
||||
passOptions.value.series.color = ['#D4D4D8', '#00C261'];
|
||||
completeOptions.value.series.color = ['#00C261', '#3370FF', '#D4D4D8'];
|
||||
}
|
||||
|
||||
const testPlanCountOptions = ref({});
|
||||
async function initTestPlanCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const params = {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
|
||||
testPlanCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTestPlanCount();
|
||||
});
|
||||
|
||||
const legendData = ref<LegendData[]>([
|
||||
{
|
||||
label: t('common.notStarted'),
|
||||
value: 'notStarted',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--primary-4))] ml-[24px]',
|
||||
onMounted(() => {
|
||||
initTestPlanCount();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initTestPlanCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initTestPlanCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initTestPlanCount();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.inProgress'),
|
||||
value: 'inProgress',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.completed'),
|
||||
value: 'completed',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
|
||||
},
|
||||
{
|
||||
label: t('common.archived'),
|
||||
value: 'archived',
|
||||
rote: 30,
|
||||
count: 3,
|
||||
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
|
||||
},
|
||||
]);
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="title"> {{ t('workbench.homePage.waitForReview') }} </div>
|
||||
<div class="title"> {{ t(props.item.label) }} </div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="projectIds"
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
|
@ -78,13 +77,30 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const projectIds = ref('');
|
||||
const projectOptions = ref<SelectOptionData[]>([]);
|
||||
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
})
|
||||
);
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
|
@ -122,10 +138,55 @@
|
|||
showSelectAll: false,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setLoadListParams({});
|
||||
loadList();
|
||||
function initData() {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
setLoadListParams({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
});
|
||||
loadList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initData();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => timeForm.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initData();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -9,18 +9,18 @@
|
|||
size="medium"
|
||||
@change="handleChangeTime"
|
||||
>
|
||||
<a-radio value="3" class="show-type-icon p-[2px]">
|
||||
<a-radio :value="3" class="show-type-icon p-[2px]">
|
||||
{{ t('workbench.homePage.nearlyThreeDays') }}
|
||||
</a-radio>
|
||||
<a-radio value="7" class="show-type-icon p-[2px]">
|
||||
<a-radio :value="7" class="show-type-icon p-[2px]">
|
||||
{{ t('workbench.homePage.nearlySevenDays') }}
|
||||
</a-radio>
|
||||
<a-radio value="customize" class="show-type-icon p-[2px]">
|
||||
<a-radio value="" class="show-type-icon p-[2px]">
|
||||
{{ t('workbench.homePage.customize') }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<a-range-picker
|
||||
v-if="timeForm.dayNumber === 'customize'"
|
||||
v-if="!timeForm.dayNumber"
|
||||
v-model:model-value="rangeTime"
|
||||
show-time
|
||||
value-format="timestamp"
|
||||
|
@ -28,43 +28,96 @@
|
|||
defaultValue: tempRange,
|
||||
}"
|
||||
class="w-[360px]"
|
||||
@select="handleTimeSelect"
|
||||
@ok="handleTimeSelect"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<MsTag
|
||||
no-margin
|
||||
:tooltip-disabled="true"
|
||||
class="h-[30px] cursor-pointer"
|
||||
theme="outline"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<MsIcon class="text-[16px] text-[var(color-text-4)]" :size="32" type="icon-icon_reset_outlined" />
|
||||
</MsTag>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary !px-[8px]" @click="cardSetting">
|
||||
<template #icon>
|
||||
<icon-settings class="setting-icon" @click="handleShowSetting" />
|
||||
<icon-settings class="setting-icon" />
|
||||
</template>
|
||||
{{ t('workbench.homePage.cardSetting') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO 等待卡片设置列表排版出来调整 -->
|
||||
<div v-if="defaultWorkList.length" class="card-content grid grid-cols-2 gap-4">
|
||||
</div>
|
||||
<div v-if="defaultWorkList.length" class="card-content mt-[12px] grid grid-cols-2 gap-4">
|
||||
<div v-for="item of defaultWorkList" :key="item.id" :class="`card-item ${item.fullScreen ? 'col-span-2' : ''}`">
|
||||
<Overview
|
||||
v-if="[WorkCardEnum.CREATE_BY_ME, WorkCardEnum.PROJECT_VIEW].includes(item.key)"
|
||||
:title="item.label"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
<OverviewMember
|
||||
v-else-if="item.key === WorkCardEnum.PROJECT_MEMBER_VIEW"
|
||||
v-model:projectIds="item.projectIds"
|
||||
v-model:handleUsers="item.handleUsers"
|
||||
:item="item"
|
||||
/>
|
||||
<CaseCount v-else-if="item.key === WorkCardEnum.CASE_COUNT" v-model:projectIds="item.projectIds" :item="item" />
|
||||
<RelatedCaseCount
|
||||
v-else-if="item.key === WorkCardEnum.ASSOCIATE_CASE_COUNT"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
<CaseReviewedCount
|
||||
v-else-if="item.key === WorkCardEnum.REVIEW_CASE_COUNT"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
<WaitReviewList
|
||||
v-else-if="item.key === WorkCardEnum.REVIEWING_BY_ME"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
<OverviewMember v-else-if="item.key === WorkCardEnum.PROJECT_MEMBER_VIEW" />
|
||||
<CaseCount v-else-if="item.key === WorkCardEnum.CASE_COUNT" />
|
||||
<RelatedCaseCount v-else-if="item.key === WorkCardEnum.ASSOCIATE_CASE_COUNT" />
|
||||
<CaseReviewedCount v-else-if="item.key === WorkCardEnum.REVIEW_CASE_COUNT" />
|
||||
<WaitReviewList v-else-if="item.key === WorkCardEnum.REVIEWING_BY_ME" />
|
||||
<ApiAndScenarioCase
|
||||
v-else-if="[WorkCardEnum.API_CASE_COUNT, WorkCardEnum.SCENARIO_COUNT].includes(item.key)"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:type="item.key"
|
||||
:item="item"
|
||||
/>
|
||||
<ApiChangeList
|
||||
v-else-if="item.key === WorkCardEnum.API_CHANGE"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
<DefectMemberBar
|
||||
v-else-if="item.key === WorkCardEnum.BUG_HANDLE_USER"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
<DefectCount
|
||||
v-else-if="countOfBug.includes(item.key)"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
:type="item.key"
|
||||
/>
|
||||
<ApiChangeList v-else-if="item.key === WorkCardEnum.API_CHANGE" />
|
||||
<DefectMemberBar v-else-if="item.key === WorkCardEnum.BUG_HANDLE_USER" />
|
||||
<DefectCount v-else-if="countOfBug.includes(item.key)" :type="item.key" />
|
||||
<ApiCount v-else-if="item.key === WorkCardEnum.API_COUNT" />
|
||||
<TestPlanCount v-else-if="item.key === WorkCardEnum.TEST_PLAN_COUNT" />
|
||||
<ApiCount v-else-if="item.key === WorkCardEnum.API_COUNT" v-model:projectIds="item.projectIds" :item="item" />
|
||||
<TestPlanCount
|
||||
v-else-if="item.key === WorkCardEnum.TEST_PLAN_COUNT"
|
||||
v-model:projectIds="item.projectIds"
|
||||
:item="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoData v-else all-screen />
|
||||
<NoData
|
||||
v-if="showNoData || !appStore.projectList.length"
|
||||
:no-res-permission="!appStore.projectList.length"
|
||||
:all-screen="!!appStore.projectList.length"
|
||||
height="h-[calc(100vh-110px)]"
|
||||
@config="cardSetting"
|
||||
/>
|
||||
</div>
|
||||
<CardSettingDrawer v-model:visible="showSettingDrawer" />
|
||||
<CardSettingDrawer v-model:visible="showSettingDrawer" :list="defaultWorkList" @success="initDefaultList" />
|
||||
<MsBackButton target=".page-content" />
|
||||
</template>
|
||||
|
||||
|
@ -72,6 +125,7 @@
|
|||
import { ref } from 'vue';
|
||||
|
||||
import MsBackButton from '@/components/pure/ms-back-button/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
import NoData from '../components/notData.vue';
|
||||
import ApiAndScenarioCase from './components/apiAndScenarioCase.vue';
|
||||
import ApiChangeList from './components/apiChangeList.vue';
|
||||
|
@ -87,9 +141,10 @@
|
|||
import DefectMemberBar from '@/views/workbench/homePage/components/defectMemberBar.vue';
|
||||
import OverviewMember from '@/views/workbench/homePage/components/overviewMember.vue';
|
||||
|
||||
import { getDashboardLayout } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useUserStore } from '@/store';
|
||||
import { getGenerateId } from '@/utils';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
|
||||
|
||||
import { SelectedCardItem } from '@/models/workbench/homePage';
|
||||
|
@ -97,15 +152,14 @@
|
|||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
// 显示设置
|
||||
function handleShowSetting() {}
|
||||
const { t } = useI18n();
|
||||
|
||||
const rangeTime = ref<number[]>([]);
|
||||
const tempRange = ref<(Date | string | number)[]>(['00:00:00', '00:00:00']);
|
||||
const initTime = {
|
||||
dayNumber: '3',
|
||||
dayNumber: 3,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
};
|
||||
|
@ -119,7 +173,7 @@
|
|||
// 改变时间类型
|
||||
function handleChangeTime(value: string | number | boolean, ev: Event) {
|
||||
resetTime();
|
||||
timeForm.value.dayNumber = value as string;
|
||||
timeForm.value.dayNumber = value as number;
|
||||
setLocalStorage(`WORK_TIME_${userStore.id}`, JSON.stringify(timeForm.value));
|
||||
}
|
||||
// 改变时间
|
||||
|
@ -151,29 +205,26 @@
|
|||
|
||||
const defaultWorkList = ref<SelectedCardItem[]>([]);
|
||||
|
||||
function initDefaultList() {
|
||||
if (userStore.isAdmin) {
|
||||
defaultWorkList.value = [
|
||||
{
|
||||
id: getGenerateId(),
|
||||
label: t('workbench.homePage.projectOverview'),
|
||||
key: WorkCardEnum.PROJECT_VIEW,
|
||||
fullScreen: true,
|
||||
isDisabledHalfScreen: true,
|
||||
projectIds: [],
|
||||
handleUsers: [],
|
||||
},
|
||||
{
|
||||
id: getGenerateId(),
|
||||
label: t('workbench.homePage.staffOverview'),
|
||||
key: WorkCardEnum.PROJECT_MEMBER_VIEW,
|
||||
fullScreen: true,
|
||||
isDisabledHalfScreen: true,
|
||||
projectIds: [],
|
||||
handleUsers: [],
|
||||
},
|
||||
];
|
||||
const showNoData = ref(false);
|
||||
async function initDefaultList() {
|
||||
try {
|
||||
appStore.showLoading();
|
||||
const result = await getDashboardLayout(appStore.currentOrgId);
|
||||
defaultWorkList.value = result;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
if (!defaultWorkList.value.length) {
|
||||
showNoData.value = true;
|
||||
}
|
||||
appStore.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function handleRefresh() {
|
||||
initDefaultList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -187,6 +238,8 @@
|
|||
rangeTime.value = [startTime, endTime];
|
||||
}
|
||||
});
|
||||
|
||||
provide('timeForm', timeForm);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
@ -194,12 +247,11 @@
|
|||
.header-setting {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
z-index: 9;
|
||||
background: var(--color-text-n9);
|
||||
.setting {
|
||||
.setting-icon {
|
||||
color: var(--color-text-4);
|
||||
background-color: var(--color-text-10);
|
||||
color: var(--color-text-1);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: rgba(var(--primary-5));
|
||||
|
@ -212,10 +264,12 @@
|
|||
|
||||
<style lang="less">
|
||||
.card-wrapper {
|
||||
margin: 16px 0;
|
||||
padding: 24px;
|
||||
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
|
||||
@apply rounded-xl bg-white;
|
||||
&.card-min-height {
|
||||
min-height: 356px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
@apply font-medium;
|
||||
|
|
|
@ -79,4 +79,31 @@ export default {
|
|||
'workbench.homePage.fullScreen': 'Full screen',
|
||||
'workbench.homePage.sort': 'Sort',
|
||||
'workbench.homePage.workNoProjectTip': 'Workbench no content, please join the project',
|
||||
'workbench.homePage.notHasData': 'No data',
|
||||
'workbench.homePage.notHasResPermission': 'No resource permission',
|
||||
'workbench.homePage.reviewRateTooltip':
|
||||
'Review Rate: Reviewed functional test cases / All functional test cases * 100%',
|
||||
'workbench.homePage.reviewPassRateTooltip':
|
||||
'Review Pass Rate: Passed reviewed test cases / All reviewed test cases * 100%',
|
||||
'workbench.homePage.associateCaseCoverRateTooltip':
|
||||
'Association Coverage Rate: Associated functional test cases / All functional test cases * 100%',
|
||||
'workbench.homePage.caseReviewCoverRateTooltip':
|
||||
'Reviewed Test Case Coverage Rate: Reviewed test cases / All test cases * 100%',
|
||||
'workbench.homePage.apiCountCoverRateTooltip':
|
||||
'API Coverage Rate: APIs (URLs) with (test cases or scenario steps) / Total APIs * 100%',
|
||||
'workbench.homePage.apiCountCompleteRateTooltip': 'API Completion Rate: Completed APIs / Total APIs * 100%',
|
||||
'workbench.homePage.apiCaseCountCoverRateTooltip':
|
||||
'API Test Case Coverage Rate: APIs with test cases / Total APIs * 100%',
|
||||
'workbench.homePage.apiCaseCountExecuteRateTooltip':
|
||||
'Test Case Execution Rate: Executed API test cases / All API test cases * 100%',
|
||||
'workbench.homePage.apiCaseCountPassRateTooltip':
|
||||
'Test Case Pass Rate: Last successful execution test cases / Total test cases * 100%',
|
||||
'workbench.homePage.scenarioCaseCountCoverRateTooltip':
|
||||
'Scenario Coverage Rate: APIs (URLs) included in scenario steps / Total APIs * 100%',
|
||||
'workbench.homePage.scenarioCaseCountExecuteRateTooltip':
|
||||
'Scenario Execution Rate: Executed scenarios / All scenarios * 100%',
|
||||
'workbench.homePage.scenarioCaseCountPassRateTooltip':
|
||||
'Scenario Pass Rate: Last successful execution scenarios / Total scenarios * 100%',
|
||||
'workbench.homePage.planCaseCountLegacyRateTooltip':
|
||||
'Legacy Rate: Unresolved defects / All associated defects * 100%',
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
'workbench.homePage.interfaceCASE': '接口 CASE',
|
||||
'workbench.homePage.interfaceScenario': '接口场景',
|
||||
'workbench.homePage.apiPlan': '接口计划',
|
||||
'workbench.homePage.bugCount': '缺陷数量',
|
||||
'workbench.homePage.bugCount': '缺陷数',
|
||||
'workbench.homePage.nearlyThreeDays': '近3天',
|
||||
'workbench.homePage.nearlySevenDays': '近7天',
|
||||
'workbench.homePage.customize': '自定义',
|
||||
|
@ -18,8 +18,8 @@ export default {
|
|||
'workbench.homePage.staff': '人员',
|
||||
'workbench.homePage.workEmptyConfig': '工作台暂无内容,立即',
|
||||
'workbench.homePage.configureWorkbench': '配置工作台',
|
||||
'workbench.homePage.useCasesCount': '关联用例数量',
|
||||
'workbench.homePage.useCasesNumber': '用例数量',
|
||||
'workbench.homePage.useCasesCount': '关联用例数',
|
||||
'workbench.homePage.useCasesNumber': '用例数',
|
||||
'workbench.homePage.reviewed': '已评审',
|
||||
'workbench.homePage.unReviewed': '未评审',
|
||||
'workbench.homePage.havePassed': '已通过',
|
||||
|
@ -30,27 +30,27 @@ export default {
|
|||
'workbench.homePage.sceneUseCase': '场景用例',
|
||||
'workbench.homePage.waitForReview': '待我评审',
|
||||
'workbench.homePage.executionTimes': '执行次数',
|
||||
'workbench.homePage.apiUseCasesNumber': '接口用例数量',
|
||||
'workbench.homePage.misstatementCount': '误报数量',
|
||||
'workbench.homePage.apiUseCasesNumber': '接口用例数',
|
||||
'workbench.homePage.misstatementCount': '误报数',
|
||||
'workbench.homePage.apiCoverage': '接口覆盖率',
|
||||
'workbench.homePage.caseExecutionRate': '用例执行率',
|
||||
'workbench.homePage.casePassedRate': '用例通过率',
|
||||
'workbench.homePage.sceneExecutionRate': '场景执行率',
|
||||
'workbench.homePage.executionRate': '执行通过率',
|
||||
'workbench.homePage.scenarioUseCasesNumber': '场景用例数量',
|
||||
'workbench.homePage.scenarioUseCasesNumber': '场景用例数',
|
||||
'workbench.homePage.interfaceChange': '接口变更',
|
||||
'workbench.homePage.associationCASE': '关联CASE',
|
||||
'workbench.homePage.associatedScene': '关联场景',
|
||||
'workbench.homePage.pendingDefect': '待我处理的缺陷',
|
||||
'workbench.homePage.defectProcessingNumber': '缺陷处理人数量',
|
||||
'workbench.homePage.defectProcessingNumber': '缺陷处理人数',
|
||||
'workbench.homePage.defectTotal': '缺陷总数',
|
||||
'workbench.homePage.legacyDefectsNumber': '遗留缺陷数',
|
||||
'workbench.homePage.createdBugByMe': '我创建的缺陷',
|
||||
'workbench.homePage.remainingBugOfPlan': '计划遗留缺陷数量',
|
||||
'workbench.homePage.apiCount': '接口数量',
|
||||
'workbench.homePage.remainingBugOfPlan': '计划遗留缺陷数',
|
||||
'workbench.homePage.apiCount': '接口数',
|
||||
'workbench.homePage.unFinish': '未完成',
|
||||
'workbench.homePage.numberOfTestPlan': '测试计划数量',
|
||||
'workbench.homePage.numberOfCaseReviews': '用例评审数量',
|
||||
'workbench.homePage.numberOfTestPlan': '测试计划数',
|
||||
'workbench.homePage.numberOfCaseReviews': '用例评审数',
|
||||
'workbench.homePage.projectOverview': '项目概览',
|
||||
'workbench.homePage.projectOverviewDesc': '统计所在项目的资源及分布的数据统计',
|
||||
'workbench.homePage.staffOverviewDesc': '统计成员在所选项目中创建的资源及分布的数据统计',
|
||||
|
@ -77,4 +77,19 @@ export default {
|
|||
'workbench.homePage.fullScreen': '全屏',
|
||||
'workbench.homePage.sort': '排序',
|
||||
'workbench.homePage.workNoProjectTip': '工作台暂无内容,请先加入项目',
|
||||
'workbench.homePage.notHasData': '无数据',
|
||||
'workbench.homePage.notHasResPermission': '无资源权限',
|
||||
'workbench.homePage.reviewRateTooltip': '评审率: 已评审功能用例/所有功能用例 * 100%',
|
||||
'workbench.homePage.reviewPassRateTooltip': '评审通过率:已评审通过的用例/所有完成评审的用例*100%',
|
||||
'workbench.homePage.associateCaseCoverRateTooltip': '覆盖率:关联的功能用例/所有功能用例 * 100%',
|
||||
'workbench.homePage.caseReviewCoverRateTooltip': '覆盖率:已评审的用例/所有的用例*100%',
|
||||
'workbench.homePage.apiCountCoverRateTooltip': '接口覆盖率:接口(URL)有(用例或场景步骤)数/接口总数*100%',
|
||||
'workbench.homePage.apiCountCompleteRateTooltip': '接口完成率:已完成的接口/接口总数*100%',
|
||||
'workbench.homePage.apiCaseCountCoverRateTooltip': '接口覆盖率:有用例的接口/接口总数*100%',
|
||||
'workbench.homePage.apiCaseCountExecuteRateTooltip': '用例执行率:执行过的接口用例/所有接口用例 * 100%',
|
||||
'workbench.homePage.apiCaseCountPassRateTooltip': '用例通过率:最后一次执行成功的用例/用例总数*100%',
|
||||
'workbench.homePage.scenarioCaseCountCoverRateTooltip': '接口覆盖率:被场景步骤包含的接口(URL)数/接口总数*100%',
|
||||
'workbench.homePage.scenarioCaseCountExecuteRateTooltip': '场景执行率:执行过的场景/所有场景 * 100%',
|
||||
'workbench.homePage.scenarioCaseCountPassRateTooltip': '场景通过率:最后一次执行成功的场景/场景总数*100%',
|
||||
'workbench.homePage.planCaseCountLegacyRateTooltip': '遗留率:未关闭缺陷/所有关联的缺陷*100%',
|
||||
};
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { commonConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { ModuleCardItem } from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum, WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
const { t } = useI18n();
|
||||
// 通用颜色配置
|
||||
export const commonColorConfig = [
|
||||
'#811FA3',
|
||||
'#00C261',
|
||||
|
@ -26,28 +33,71 @@ export const commonColorConfig = [
|
|||
'#87F578',
|
||||
];
|
||||
|
||||
export const defectStatusColor = ['#00C261', '#FFA200'];
|
||||
// 饼图颜色配置
|
||||
export const colorMapConfig: Record<string, string[]> = {
|
||||
[WorkCardEnum.CASE_COUNT]: ['#ED0303', '#FFA200', '#3370FF', '#D4D4D8'],
|
||||
[WorkCardEnum.ASSOCIATE_CASE_COUNT]: ['#00C261', '#3370FF'],
|
||||
[WorkCardEnum.REVIEW_CASE_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.TEST_PLAN_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.PLAN_LEGACY_BUG]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.BUG_COUNT]: ['#FFA200', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.HANDLE_BUG_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.CREATE_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.API_COUNT]: ['#811FA3', '#00C261', '#3370FF', '#FFA1FF', '#EE50A3', '#FF9964', '#F9F871', '#C3DD40'],
|
||||
[WorkCardEnum.CREATE_BUG_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
};
|
||||
|
||||
// 柱状图
|
||||
export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<string, any> {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
tooltip: [
|
||||
{
|
||||
trigger: 'axis',
|
||||
borderWidth: 0,
|
||||
padding: 0,
|
||||
label: {
|
||||
width: 50,
|
||||
overflow: 'truncate',
|
||||
},
|
||||
displayMode: 'single',
|
||||
enterable: true,
|
||||
// TODO 单例模式
|
||||
// formatter(params: any) {
|
||||
// const html = `
|
||||
// <div class="w-[186px] h-[50px] p-[16px] flex items-center justify-between">
|
||||
// <div class=" flex items-center">
|
||||
// <div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm bg-[${params.color}]" style="background:${
|
||||
// params.color
|
||||
// }"></div>
|
||||
// <div style="color:#959598">${params.name}</div>
|
||||
// </div>
|
||||
// <div class="text-[#323233] font-medium">${addCommasToNumber(params.value)}</div>
|
||||
// </div>
|
||||
// `;
|
||||
// return html;
|
||||
// },
|
||||
formatter(params: any) {
|
||||
const html = `
|
||||
<div class="w-[186px] h-[50px] p-[16px] flex items-center justify-between">
|
||||
<div class="w-[186px] ms-scroll-bar max-h-[206px] overflow-y-auto p-[16px] gap-[8px] flex flex-col">
|
||||
${params
|
||||
.map(
|
||||
(item: any) => `
|
||||
<div class="flex h-[18px] items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm bg-[${params.color}]" style="background:${
|
||||
params.color
|
||||
}"></div>
|
||||
<div style="color:#959598">${params.name}</div>
|
||||
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm" style="background:${item.color}"></div>
|
||||
<div class="one-line-text max-w-[120px]" style="color:#959598">${item.seriesName}</div>
|
||||
</div>
|
||||
<div class="text-[#323233] font-medium">${addCommasToNumber(params.value)}</div>
|
||||
<div class="text-[#323233] font-medium">${addCommasToNumber(item.value)}</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
return html;
|
||||
},
|
||||
},
|
||||
],
|
||||
color,
|
||||
grid: {
|
||||
top: '36px',
|
||||
|
@ -63,6 +113,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
data: [],
|
||||
axisLabel: {
|
||||
color: '#646466',
|
||||
interval: 0,
|
||||
},
|
||||
axisTick: {
|
||||
show: false, // 隐藏刻度线
|
||||
|
@ -81,7 +132,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#AEAEB2', // 自定义字体大小和颜色
|
||||
padding: [0, 0, 0, -20], // 通过左侧(最后一个值)的负偏移向左移动
|
||||
padding: [0, 0, 0, 10], // 通过padding控制Y轴单位距离左侧的距离
|
||||
},
|
||||
nameGap: 20,
|
||||
splitLine: {
|
||||
|
@ -94,14 +145,41 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
},
|
||||
},
|
||||
],
|
||||
graphic: {
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
style: {
|
||||
text: '',
|
||||
fontSize: 14,
|
||||
fill: '#959598',
|
||||
backgroundColor: '#F9F9FE',
|
||||
padding: [6, 16, 6, 16],
|
||||
borderRadius: 4,
|
||||
},
|
||||
invisible: true,
|
||||
},
|
||||
|
||||
colorBy: 'series',
|
||||
series: [],
|
||||
barCategoryGap: '50%', // 控制 X 轴分布居中效果
|
||||
legend: {
|
||||
width: '60%',
|
||||
show: true,
|
||||
type: 'scroll',
|
||||
itemGap: 20,
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
left: 'center',
|
||||
pageButtonItemGap: 5,
|
||||
pageButtonGap: 5,
|
||||
pageIconColor: '#00000099',
|
||||
pageIconInactiveColor: '#00000042',
|
||||
pageIconSize: [10, 8],
|
||||
pageTextStyle: {
|
||||
color: '#00000099',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
dataZoom: hasRoom
|
||||
? [
|
||||
|
@ -116,4 +194,353 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
};
|
||||
}
|
||||
|
||||
export default {};
|
||||
export const contentTabList = ref<ModuleCardItem[]>([
|
||||
{
|
||||
label: t('workbench.homePage.functionalUseCase'),
|
||||
value: WorkOverviewEnum.FUNCTIONAL,
|
||||
icon: WorkOverviewIconEnum.FUNCTIONAL,
|
||||
color: 'rgb(var(--primary-5))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.useCaseReview'),
|
||||
value: WorkOverviewEnum.CASE_REVIEW,
|
||||
icon: WorkOverviewIconEnum.CASE_REVIEW,
|
||||
color: 'rgb(var(--success-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceAPI'),
|
||||
value: WorkOverviewEnum.API,
|
||||
icon: WorkOverviewIconEnum.API,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceCASE'),
|
||||
value: WorkOverviewEnum.API_CASE,
|
||||
icon: WorkOverviewIconEnum.API_CASE,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.interfaceScenario'),
|
||||
value: WorkOverviewEnum.API_SCENARIO,
|
||||
icon: WorkOverviewIconEnum.API_SCENARIO,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.apiPlan'),
|
||||
value: WorkOverviewEnum.TEST_PLAN,
|
||||
icon: WorkOverviewIconEnum.TEST_PLAN,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.bugCount'),
|
||||
value: WorkOverviewEnum.BUG_COUNT,
|
||||
icon: WorkOverviewIconEnum.BUG_COUNT,
|
||||
color: 'rgb(var(--danger-6))',
|
||||
count: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
// 下方饼图配置
|
||||
export function getPieCharOptions(key: WorkCardEnum) {
|
||||
return {
|
||||
title: {
|
||||
show: true,
|
||||
text: '总数(个)',
|
||||
left: 85,
|
||||
top: '30%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
color: '#959598',
|
||||
},
|
||||
subtext: '100111',
|
||||
subtextStyle: {
|
||||
fontSize: 20,
|
||||
color: '#323233',
|
||||
fontWeight: 'bold',
|
||||
align: 'center',
|
||||
},
|
||||
textAlign: 'center', // 确保副标题居中
|
||||
},
|
||||
color: colorMapConfig[key],
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
position: 'right',
|
||||
},
|
||||
legend: {
|
||||
width: '100%',
|
||||
height: 128,
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
pageButtonItemGap: 5,
|
||||
pageButtonGap: 5,
|
||||
pageIconColor: '#00000099',
|
||||
pageIconInactiveColor: '#00000042',
|
||||
pageIconSize: [7, 5],
|
||||
pageTextStyle: {
|
||||
color: '#00000099',
|
||||
fontSize: 12,
|
||||
},
|
||||
pageButtonPosition: 'end',
|
||||
itemGap: 16,
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
icon: 'circle',
|
||||
bottom: 'center',
|
||||
left: 180,
|
||||
textStyle: {
|
||||
color: '#333',
|
||||
fontSize: 14, // 字体大小
|
||||
textBorderType: 'solid',
|
||||
rich: {
|
||||
a: {
|
||||
width: 50,
|
||||
color: '#959598',
|
||||
fontSize: 12,
|
||||
align: 'left',
|
||||
},
|
||||
b: {
|
||||
width: 50,
|
||||
color: '#323233',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
align: 'right',
|
||||
},
|
||||
c: {
|
||||
width: 50,
|
||||
color: '#323233',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
align: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
media: [
|
||||
{
|
||||
query: { maxWidth: 600 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: { minWidth: 601, maxWidth: 800 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 450,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: { minWidth: 801, maxWidth: 1200 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: { minWidth: 1201 },
|
||||
option: {
|
||||
legend: {
|
||||
textStyle: {
|
||||
width: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
padAngle: 2,
|
||||
radius: ['75%', '90%'],
|
||||
center: [90, '48%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 空数据和无权限处理
|
||||
export function handleNoDataDisplay(
|
||||
xAxis: string[],
|
||||
projectCountList: { id: string; name: string; count: number[] }[]
|
||||
) {
|
||||
if (!xAxis.length) {
|
||||
return {
|
||||
invisible: false,
|
||||
text: t('workbench.homePage.notHasResPermission'),
|
||||
};
|
||||
}
|
||||
|
||||
const isEmptyData = projectCountList.every((item) =>
|
||||
item.count.every((e) => e === 0 || e === null || e === undefined)
|
||||
);
|
||||
|
||||
if (isEmptyData) {
|
||||
return {
|
||||
invisible: false,
|
||||
text: t('workbench.homePage.notHasData'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
invisible: true,
|
||||
text: '',
|
||||
};
|
||||
}
|
||||
|
||||
// XX率饼图配置
|
||||
export const commonRatePieOptions = {
|
||||
...commonConfig,
|
||||
title: {
|
||||
show: true,
|
||||
text: '',
|
||||
left: 26,
|
||||
top: '20%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
color: '#959598',
|
||||
},
|
||||
triggerEvent: true, // 开启鼠标事件
|
||||
subtext: '0',
|
||||
subtextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#323233',
|
||||
fontWeight: 'bold',
|
||||
align: 'center',
|
||||
lineHeight: 3,
|
||||
},
|
||||
textAlign: 'center',
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
position: 'right',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
...toolTipConfig,
|
||||
position: 'right',
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
color: [],
|
||||
padAngle: 2,
|
||||
radius: ['85%', '100%'],
|
||||
center: [30, '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
scale: false, // 禁用放大效果
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
// graphic: [
|
||||
// {
|
||||
// type: 'text',
|
||||
// left: 'center',
|
||||
// top: '5%',
|
||||
// style: {
|
||||
// text: '饼图标题',
|
||||
// fontSize: 18,
|
||||
// fontWeight: 'bold',
|
||||
// fill: '#333',
|
||||
// cursor: 'pointer'
|
||||
// },
|
||||
// onmouseover (params) {
|
||||
// // 悬浮到标题上时显示提示信息
|
||||
// // chart.dispatchAction({
|
||||
// // type: 'showTip',
|
||||
// // position: [params.event.offsetX, params.event.offsetY],
|
||||
// // // 配置提示内容
|
||||
// // formatter: '这是饼图标题的提示内容'
|
||||
// // });
|
||||
// },
|
||||
// onmouseout () {
|
||||
// // 离开标题时隐藏提示信息
|
||||
// // chart.dispatchAction({
|
||||
// // type: 'hideTip'
|
||||
// // });
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
};
|
||||
|
||||
// 统一处理下方饼图数据结构
|
||||
export function handlePieData(
|
||||
key: WorkCardEnum,
|
||||
statusPercentList: {
|
||||
status: string; // 状态
|
||||
count: number;
|
||||
percentValue: string; // 百分比
|
||||
}[]
|
||||
) {
|
||||
const options: Record<string, any> = getPieCharOptions(key);
|
||||
options.series.data = statusPercentList.map((item) => ({
|
||||
name: item.status,
|
||||
value: item.count,
|
||||
}));
|
||||
|
||||
// 计算总数和图例格式
|
||||
const tempObject: Record<string, any> = {};
|
||||
let totalCount = 0;
|
||||
statusPercentList.forEach((item) => {
|
||||
tempObject[item.status] = item;
|
||||
totalCount += item.count;
|
||||
});
|
||||
|
||||
// 设置图例的格式化函数,显示百分比
|
||||
options.legend.formatter = (name: string) => {
|
||||
return `{a|${tempObject[name].status}} {b|${addCommasToNumber(tempObject[name].count)}} {c|${
|
||||
tempObject[name].percentValue
|
||||
}}`;
|
||||
};
|
||||
|
||||
// 设置副标题为总数
|
||||
options.title.subtext = addCommasToNumber(totalCount);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -26,20 +26,20 @@
|
|||
v-if="features.includes(FeatureEnum.TEST_PLAN)"
|
||||
:project="currentProject"
|
||||
:refresh-id="refreshId"
|
||||
type="my_follow"
|
||||
type="my_todo"
|
||||
hide-show-type
|
||||
/>
|
||||
<caseReviewTable
|
||||
v-if="features.includes(FeatureEnum.CASE_REVIEW)"
|
||||
:project="currentProject"
|
||||
:refresh-id="refreshId"
|
||||
type="my_follow"
|
||||
type="my_todo"
|
||||
/>
|
||||
<bugTable
|
||||
v-if="features.includes(FeatureEnum.BUG)"
|
||||
:project="currentProject"
|
||||
:refresh-id="refreshId"
|
||||
type="my_follow"
|
||||
type="my_todo"
|
||||
/>
|
||||
</template>
|
||||
<NoData v-else all-screen />
|
||||
|
|
Loading…
Reference in New Issue