feat(工作台): 工作台联调部分

This commit is contained in:
xinxin.wu 2024-11-12 19:34:59 +08:00 committed by Craftsman
parent dc58054aef
commit a8e718a961
33 changed files with 2419 additions and 1482 deletions

View File

@ -7,8 +7,17 @@ import type { ReviewItem } from '@/models/caseManagement/caseReview';
import type { CaseManagementTable } from '@/models/caseManagement/featureCase'; import type { CaseManagementTable } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common'; import type { CommonList, TableQueryParams } from '@/models/common';
import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan'; import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan';
import type {
OverViewOfProject,
PassRateDataType,
SelectedCardItem,
WorkHomePageDetail,
} from '@/models/workbench/homePage';
import { import {
EditDashboardLayoutUrl,
GetDashboardLayoutUrl,
WorkAssociateCaseDetailUrl,
WorkbenchApiCaseListUrl, WorkbenchApiCaseListUrl,
WorkbenchBugListUrl, WorkbenchBugListUrl,
WorkbenchCaseListUrl, WorkbenchCaseListUrl,
@ -16,6 +25,13 @@ import {
WorkbenchScenarioListUrl, WorkbenchScenarioListUrl,
WorkbenchTestPlanListUrl, WorkbenchTestPlanListUrl,
WorkbenchTestPlanStatisticUrl, WorkbenchTestPlanStatisticUrl,
WorkCaseCountDetailUrl,
WorkMemberViewDetailUrl,
WorkMyCreatedDetailUrl,
WorkProOverviewDetailUrl,
WorkTodoBugListUrl,
WorkTodoPlanListUrl,
WorkTodoReviewListUrl,
} from '../requrls/workbench'; } from '../requrls/workbench';
// 我的-场景列表 // 我的-场景列表
@ -52,3 +68,49 @@ export function workbenchBugList(data: TableQueryParams) {
export function workbenchApiCaseList(data: TableQueryParams) { export function workbenchApiCaseList(data: TableQueryParams) {
return MSR.post<CommonList<ApiCaseDetail>>({ url: WorkbenchApiCaseListUrl, data }); 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 });
}

View File

@ -5,3 +5,13 @@ export const WorkbenchTestPlanStatisticUrl = '/dashboard/my/plan/statistics'; //
export const WorkbenchCaseListUrl = '/dashboard/my/functional/page'; // 工作台-我的-用例列表 export const WorkbenchCaseListUrl = '/dashboard/my/functional/page'; // 工作台-我的-用例列表
export const WorkbenchBugListUrl = '/dashboard/my/bug/page'; // 工作台-我的-缺陷列表 export const WorkbenchBugListUrl = '/dashboard/my/bug/page'; // 工作台-我的-缺陷列表
export const WorkbenchApiCaseListUrl = '/dashboard/my/api/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'; // 工作台-首页-关联用例数量

View File

@ -259,6 +259,7 @@ export default function useTableProps<T>(
return data; return data;
} }
} catch (err) { } catch (err) {
// TODO 在这里处理拦截设置表格无资源权限
setTableErrorStatus('error'); setTableErrorStatus('error');
propsRes.value.data = []; propsRes.value.data = [];
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -1,3 +1,4 @@
import { TableQueryParams } from '@/models/common';
import { WorkCardEnum } from '@/enums/workbenchEnum'; import { WorkCardEnum } from '@/enums/workbenchEnum';
// 配置卡片列表 // 配置卡片列表
@ -27,3 +28,55 @@ export interface SelectedCardItem {
projectIds: string[]; projectIds: string[];
handleUsers: 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; // 百分比
}[];
}

View File

@ -277,8 +277,13 @@ const useAppStore = defineStore('app', {
try { try {
const { isWhiteListPage } = useUser(); const { isWhiteListPage } = useUser();
const routeName = router.currentRoute.value.name as string; const routeName = router.currentRoute.value.name as string;
if (!this.currentProjectId || routeName?.includes('setting') || isWhiteListPage()) { if (
// 如果没有项目id或访问的是系统设置下的页面/白名单页面,则不读取项目基础信息 !this.currentProjectId ||
routeName?.includes('setting') ||
routeName?.includes('workstation') ||
isWhiteListPage()
) {
// 如果没有项目id或访问的是系统设置下的页面/白名单/工作台页面,则不读取项目基础信息
return; return;
} }
const res = await getProjectInfo(this.currentProjectId); const res = await getProjectInfo(this.currentProjectId);

View File

@ -244,18 +244,19 @@ const useUserStore = defineStore('user', {
const { isLoginPage } = useUser(); const { isLoginPage } = useUser();
const appStore = useAppStore(); const appStore = useAppStore();
const isLogin = await this.isLogin(forceSet); const isLogin = await this.isLogin(forceSet);
const routeName = router.currentRoute.value.name as string;
if (isLogin && appStore.currentProjectId !== 'no_such_project') { if (isLogin && appStore.currentProjectId !== 'no_such_project') {
// 当前为登陆状态,且已经选择了项目,初始化当前项目配置 // 当前为登陆状态,且已经选择了项目,初始化当前项目配置
try { try {
const HasProjectPermission = await getUserHasProjectPermission(appStore.currentProjectId); const HasProjectPermission = await getUserHasProjectPermission(appStore.currentProjectId);
if (!HasProjectPermission) { // 无权限&&工作台不会跳转无资源
if (!HasProjectPermission && !routeName?.includes('workstation')) {
// 没有项目权限(用户所在的当前项目被禁用&用户被移除出去该项目) // 没有项目权限(用户所在的当前项目被禁用&用户被移除出去该项目)
router.push({ router.push({
name: NO_PROJECT_ROUTE_NAME, name: NO_PROJECT_ROUTE_NAME,
}); });
return; return;
} }
const routeName = router.currentRoute.value.name as string;
if (routeName?.includes('setting')) { if (routeName?.includes('setting')) {
// 访问系统设置下的页面,不需要获取项目信息,会在切换到非系统设置页面时获取(ms-menu组件内初始化会获取) // 访问系统设置下的页面,不需要获取项目信息,会在切换到非系统设置页面时获取(ms-menu组件内初始化会获取)
appStore.setCurrentMenuConfig([]); appStore.setCurrentMenuConfig([]);

View File

@ -44,7 +44,7 @@
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import { getCustomFieldHeader, getCustomOptionHeader } from '@/api/modules/bug-management'; 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 { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -192,8 +192,12 @@
columns.splice(2, 0, ...customColumns); columns.splice(2, 0, ...customColumns);
await initFilterOptions(); await initFilterOptions();
const workbenchBugPage = computed(() => {
return props.type === 'my_todo' ? workbenchTodoBugList : workbenchBugList;
});
const { propsRes, propsEvent, setLoadListParams, loadList } = useTable( const { propsRes, propsEvent, setLoadListParams, loadList } = useTable(
workbenchBugList, workbenchBugPage.value,
{ {
columns, columns,
scroll: { x: '100%' }, scroll: { x: '100%' },
@ -237,6 +241,7 @@
setLoadListParams({ setLoadListParams({
projectId: props.project, projectId: props.project,
viewId: props.type, viewId: props.type,
myTodo: props.type === 'my_todo',
}); });
loadList(); loadList();
} }

View File

@ -68,7 +68,7 @@
import MsStatusTag from '@/components/business/ms-status-tag/index.vue'; import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.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 { reviewStatusMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; 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, columns,
scroll: { x: '100%' }, scroll: { x: '100%' },
showSetting: false, showSetting: false,
@ -179,6 +183,7 @@
setLoadListParams({ setLoadListParams({
projectId: props.project, projectId: props.project,
viewId: props.type, viewId: props.type,
myTodo: props.type === 'my_todo',
}); });
loadList(); loadList();
} }

View File

@ -1,5 +1,5 @@
<template> <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="flex h-full w-full flex-col items-center justify-center">
<div class="no-config-svg"></div> <div class="no-config-svg"></div>
<div class="flex items-center"> <div class="flex items-center">
@ -14,15 +14,21 @@
class="ml-[8px] font-medium" class="ml-[8px] font-medium"
@click="() => emit('config')" @click="() => emit('config')"
> >
{{ t('workbench.homePage.configureWorkbench') }} {{ t('workbench.homePage.cardSetting') }}
</MsButton> </MsButton>
</div> </div>
</div> </div>
</MsCard> </MsCard>
<div v-else-if="props.isDashboard" class="no-card"> <div v-else-if="props.noResPermission || props.isDashboard" :class="`${props.height || 'h-full'} w-full`">
<div class="no-card-svg"></div> <div class="no-card">
<div class="font-medium text-[var(--color-text-1)]">{{ t('workbench.homePage.noCard') }}</div> <div :class="`${props.noResPermission ? 'no-permission-svg' : 'no-card-svg'}`"></div>
<div class="text-[var(--color-text-4)]">{{ t('workbench.homePage.noCardDesc') }}</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>
<div v-else class="not-setting-data"> <div v-else class="not-setting-data">
{{ t('workbench.homePage.noDataTemporarily') }} {{ t('workbench.homePage.noDataTemporarily') }}
@ -39,6 +45,8 @@
const props = defineProps<{ const props = defineProps<{
allScreen?: boolean; allScreen?: boolean;
isDashboard?: boolean; // isDashboard?: boolean; //
noResPermission?: boolean; //
height?: string;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -65,7 +73,6 @@
background-size: cover; background-size: cover;
} }
.no-card { .no-card {
margin-top: -10%;
@apply flex h-full w-full flex-col items-center justify-center gap-4; @apply flex h-full w-full flex-col items-center justify-center gap-4;
.no-card-svg { .no-card-svg {
margin: 0 auto; margin: 0 auto;
@ -74,5 +81,12 @@
background: url('@/assets/svg/work-no-card.svg'); background: url('@/assets/svg/work-no-card.svg');
background-size: cover; background-size: cover;
} }
.no-permission-svg {
margin: 0 auto;
width: 160px;
height: 90px;
background: url('@/assets/svg/no_resource.svg');
background-size: cover;
}
} }
</style> </style>

View File

@ -129,7 +129,11 @@
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue'; import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
import StatusProgress from '@/views/test-plan/testPlan/components/statusProgress.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 { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
@ -246,7 +250,11 @@
showSelectorAll: false, 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(() => { const planData = computed(() => {
return propsRes.value.data; return propsRes.value.data;
@ -293,6 +301,7 @@
type: showType.value, type: showType.value,
projectId: props.project, projectId: props.project,
viewId: props.type, viewId: props.type,
myTodo: props.type === 'my_todo',
}); });
loadList(); loadList();
} }

View File

@ -1,18 +1,13 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper card-min-height">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> <div class="title">
{{ {{ t(props.item.label) }}
props.type === WorkCardEnum.API_CASE_COUNT
? t('workbench.homePage.apiUseCasesNumber')
: t('workbench.homePage.scenarioUseCasesNumber')
}}
</div> </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -26,23 +21,15 @@
<div class="mt-[16px]"> <div class="mt-[16px]">
<div class="case-count-wrapper"> <div class="case-count-wrapper">
<div class="case-count-item"> <div class="case-count-item">
<div class="case-count-item-title">{{ t('workbench.homePage.executionTimes') }}</div> <div v-for="(ele, index) of executionTimeValue" :key="index" class="case-count-item-content">
<div class="case-count-item-number">{{ addCommasToNumber(executionTimes) }}</div> <div class="case-count-item-title">{{ ele.name }}</div>
</div> <div class="case-count-item-number">{{ addCommasToNumber(ele.count) }}</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>
<div class="case-count-item-count"> </div>
<div class="case-count-item-title">{{ t('workbench.homePage.misstatementCount') }}</div> <div class="case-count-item">
<div class="case-count-item-number">{{ addCommasToNumber(executionTimes) }}</div> <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> </div>
</div> </div>
@ -74,66 +61,82 @@
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { addCommasToNumber } from '@/utils'; import { addCommasToNumber } from '@/utils';
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { WorkCardEnum } from '@/enums/workbenchEnum'; import { WorkCardEnum } from '@/enums/workbenchEnum';
const appStore = useAppStore(); const appStore = useAppStore();
const projectIds = ref('');
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ 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, value: 0,
name: t('workbench.homePage.notCover'), name: t('workbench.homePage.notCover'),
itemStyle: {
color: '#D4D4D8',
},
}, },
{ {
value: 0, value: 0,
name: t('workbench.homePage.covered'), name: t('workbench.homePage.covered'),
itemStyle: {
color: '#00C261',
},
}, },
]); ]);
const caseExecuteData = ref([ const caseExecuteData = ref<{ name: string; value: number }[]>([
{ {
value: 0, value: 0,
name: t('common.unExecute'), name: t('common.unExecute'),
itemStyle: {
color: '#D4D4D8',
},
}, },
{ {
value: 0, value: 0,
name: t('common.executed'), name: t('common.executed'),
itemStyle: {
color: '#00C261',
},
}, },
]); ]);
const casePassData = ref([ const casePassData = ref<{ name: string; value: number }[]>([
{ {
value: 0, value: 0,
name: t('workbench.homePage.notPass'), name: t('workbench.homePage.notPass'),
itemStyle: {
color: '#ED0303',
},
}, },
{ {
value: 0, value: 0,
name: t('workbench.homePage.havePassed'), name: t('workbench.homePage.havePassed'),
itemStyle: {
color: '#00C261',
},
}, },
]); ]);
@ -141,30 +144,71 @@
return { return {
name: t('workbench.homePage.apiCoverage'), name: t('workbench.homePage.apiCoverage'),
count: '80%', count: '80%',
color: ['#EDEDF1', '#00C261'],
}; };
}); });
const executeTitleConfig = computed(() => { const executeTitleConfig = computed(() => {
return props.type === WorkCardEnum.API_CASE_COUNT return props.item.key === WorkCardEnum.API_CASE_COUNT
? { ? {
name: t('workbench.homePage.caseExecutionRate'), name: t('workbench.homePage.caseExecutionRate'),
count: '80%', count: '80%',
color: ['#EDEDF1', '#00C261'],
} }
: { : {
name: t('workbench.homePage.sceneExecutionRate'), name: t('workbench.homePage.sceneExecutionRate'),
count: '80%', count: '80%',
color: ['#EDEDF1', '#00C261'],
}; };
}); });
const casePassTitleConfig = computed(() => { const casePassTitleConfig = computed(() => {
return props.type === WorkCardEnum.API_CASE_COUNT return props.item.key === WorkCardEnum.API_CASE_COUNT
? { ? {
name: t('workbench.homePage.casePassedRate'), name: t('workbench.homePage.casePassedRate'),
count: '80%', count: '80%',
color: ['#00C261', '#ED0303'],
} }
: { : {
name: t('workbench.homePage.executionRate'), name: t('workbench.homePage.executionRate'),
count: '80%', 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> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -174,18 +218,21 @@
padding: 16px; padding: 16px;
border-radius: 6px; border-radius: 6px;
background: var(--color-text-n9); background: var(--color-text-n9);
@apply flex-1; @apply flex items-center;
.case-count-item-count { .case-count-item-content {
@apply flex-1; @apply flex-1;
} .case-count-item-count {
.case-count-item-title { @apply flex-1;
margin-bottom: 8px; }
color: var(--color-text-4); .case-count-item-title {
} margin-bottom: 8px;
.case-count-item-number { color: var(--color-text-4);
font-size: 20px; }
color: var(--color-text-1); .case-count-item-number {
@apply font-medium; font-size: 20px;
color: var(--color-text-1);
@apply font-medium;
}
} }
} }
} }

View File

@ -1,14 +1,13 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper card-min-height">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> <div class="title">
{{ t('workbench.homePage.interfaceChange') }} {{ t(props.item.label) }}
</div> </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -53,12 +52,29 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
const { t } = useI18n(); const { t } = useI18n();
const projectIds = ref('');
const appStore = useAppStore(); 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 = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
@ -119,11 +135,57 @@
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'), 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(() => { onMounted(() => {
setLoadListParams({}); initData();
loadList();
}); });
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> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,12 +1,11 @@
<template> <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="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.apiCount') }} </div> <div class="title"> {{ t('workbench.homePage.apiCount') }} </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -18,15 +17,19 @@
</div> </div>
</div> </div>
<div class="my-[16px]"> <div class="my-[16px]">
<div class="case-count-wrapper"> <TabCard :content-tab-list="apiCountTabList" not-has-padding hidden-border min-width="270px">
<div class="case-count-item"> <template #item="{ item: tabItem }">
<PassRatePie :options="options" :size="60" :value-list="coverValueList" /> <div class="w-full">
</div> <PassRatePie
<div class="case-count-item"> :tooltip-text="tabItem.tooltip"
<PassRatePie :options="options" :size="60" :value-list="passValueList" /> :options="tabItem.options"
</div> :size="60"
</div> :value-list="tabItem.valueList"
<div class="mt-[16px] h-[148px]"> />
</div>
</template>
</TabCard>
<div class="h-[148px]">
<MsChart :options="apiCountOptions" /> <MsChart :options="apiCountOptions" />
</div> </div>
</div> </div>
@ -38,53 +41,68 @@
* @desc 接口数量 * @desc 接口数量
*/ */
import { ref } from 'vue'; import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsChart from '@/components/pure/chart/index.vue'; import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue'; import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.vue';
import { commonConfig, toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { 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 { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
const projectIds = ref(''); const innerProjectIds = defineModel<string[]>('projectIds', {
const projectOptions = ref<SelectOptionData[]>([]); required: true,
});
const options = ref({ const projectId = ref<string>(innerProjectIds.value[0]);
...commonConfig,
tooltip: { const timeForm = inject<Ref<TimeFormParams>>(
...toolTipConfig, 'timeForm',
}, ref({
legend: { dayNumber: 3,
show: false, startTime: 0,
}, endTime: 0,
series: { })
name: '', );
type: 'pie',
radius: ['80%', '100%'], const options = ref(cloneDeep(commonRatePieOptions));
avoidLabelOverlap: false,
label: { // TODO
show: false, const detail = ref<PassRateDataType>({
position: 'center', statusStatisticsMap: {
}, cover: [
emphasis: { { name: '覆盖率', count: 10 },
label: { { name: '已覆盖', count: 2 },
show: false, { name: '未覆盖', count: 1 },
fontSize: 40, ],
fontWeight: 'bold', success: [
}, { name: '覆盖率', count: 10 },
}, { name: '已覆盖', count: 2 },
labelLine: { { name: '未覆盖', count: 1 },
show: false, ],
},
data: [],
}, },
statusPercentList: [
{ status: 'HTTP', count: 1, percentValue: '10%' },
{ status: 'TCP', count: 3, percentValue: '0%' },
{ status: 'BBB', count: 6, percentValue: '0%' },
],
}); });
const coverValueList = ref([ const coverValueList = ref([
@ -111,148 +129,114 @@
value: 2000, value: 2000,
}, },
]); ]);
const coverOptions = ref<Record<string, any>>(cloneDeep(options.value));
const apiCountOptions = ref({ const completeOptions = ref<Record<string, any>>(cloneDeep(options.value));
title: { const apiCountTabList = computed(() => {
show: true, return [
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: [
{ {
query: { maxWidth: 600 }, label: '',
option: { value: 'execution',
legend: { valueList: coverValueList.value,
textStyle: { options: { ...coverOptions.value },
width: 200, tooltip: 'workbench.homePage.apiCountCoverRateTooltip',
},
},
},
}, },
{ {
query: { minWidth: 601, maxWidth: 800 }, label: '',
option: { value: 'pass',
legend: { valueList: passValueList.value,
textStyle: { options: { ...completeOptions.value },
width: 450, tooltip: 'workbench.homePage.apiCountCompleteRateTooltip',
},
},
},
}, },
{ ];
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: [],
},
}); });
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> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -14,7 +14,7 @@
<a-button type="secondary" @click="exitHandler"> <a-button type="secondary" @click="exitHandler">
{{ t('workbench.homePage.exitEdit') }} {{ t('workbench.homePage.exitEdit') }}
</a-button> </a-button>
<a-button type="primary" :loading="confirmLoading" @click="saveHandler"> <a-button type="primary" :disabled="!selectedCardList.length" :loading="confirmLoading" @click="saveHandler">
{{ t('common.save') }} {{ t('common.save') }}
</a-button> </a-button>
</div> </div>
@ -39,9 +39,15 @@
> >
<div class="card-item-text"> <div class="card-item-text">
<a-tooltip :content="t('workbench.homePage.sort')" :mouse-enter-delay="300"> <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> </a-tooltip>
<div>{{ item.label }}</div> <div>{{ t(item.label) }}</div>
</div> </div>
<div class="card-item-text"> <div class="card-item-text">
<a-radio-group <a-radio-group
@ -83,12 +89,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import { VueDraggable } from 'vue-draggable-plus'; import { VueDraggable } from 'vue-draggable-plus';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import NotData from '../../components/notData.vue'; import NotData from '../../components/notData.vue';
import CardSettingList from '@/views/workbench/homePage/components/cardSettingList.vue'; import CardSettingList from '@/views/workbench/homePage/components/cardSettingList.vue';
import { editDashboardLayout } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { getGenerateId } from '@/utils'; import { getGenerateId } from '@/utils';
@ -100,6 +108,14 @@
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{
list: SelectedCardItem[];
}>();
const emit = defineEmits<{
(e: 'success'): void;
}>();
const innerVisible = defineModel<boolean>('visible', { const innerVisible = defineModel<boolean>('visible', {
required: true, required: true,
}); });
@ -143,17 +159,36 @@
// //
async function saveHandler() { async function saveHandler() {
try { try {
confirmLoading.value = true;
selectedCardList.value = selectedCardList.value.map((e, pos) => { selectedCardList.value = selectedCardList.value.map((e, pos) => {
return { return {
...e, ...e,
pos, pos,
}; };
}); });
await editDashboardLayout(selectedCardList.value, appStore.currentOrgId);
emit('success');
innerVisible.value = false;
Message.success(t('common.saveSuccess')); Message.success(t('common.saveSuccess'));
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
confirmLoading.value = false;
} }
} }
watch(
() => innerVisible.value,
(val) => {
if (val) {
selectedCardList.value = cloneDeep(props.list);
}
},
{
deep: true,
}
);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -11,11 +11,11 @@
</div> </div>
<div class="card-config-menu-wrapper"> <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"> <a-sub-menu v-for="item of filteredConfigList" :key="item.value">
<template #title> <template #title>
<div class="font-medium text-[var(--color-text-1)]"> <div class="font-medium text-[var(--color-text-1)]">
{{ item.label }} {{ t(item.label) }}
</div> </div>
</template> </template>
@ -25,15 +25,22 @@
<svg-icon width="98px" height="69px" :name="ele.img" /> <svg-icon width="98px" height="69px" :name="ele.img" />
</div> </div>
<div class="card-config-text"> <div class="card-config-text">
<div>{{ ele.label }}</div> <div>{{ t(ele.label) }}</div>
<div class="card-config-desc flex"> <div class="card-config-desc flex">
<div>{{ ele.description }}</div> <div>{{ t(ele.description || '') }}</div>
</div> </div>
</div> </div>
</div> </div>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
</a-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> </div>
</template> </template>
@ -53,144 +60,149 @@
}>(); }>();
const configList = ref<WorkConfigCard[]>([ const configList = ref<WorkConfigCard[]>([
//
{ {
label: t('workbench.homePage.overview'), label: 'workbench.homePage.overview',
value: 'overview', value: 'overview',
description: '', description: '',
img: '', img: '',
children: [ children: [
{ {
label: t('workbench.homePage.projectOverview'), label: 'workbench.homePage.projectOverview',
value: WorkCardEnum.PROJECT_VIEW, value: WorkCardEnum.PROJECT_VIEW,
description: t('workbench.homePage.projectOverviewDesc'), description: 'workbench.homePage.projectOverviewDesc',
img: 'project-overview-img', img: 'project-overview-img',
}, },
{ {
label: t('workbench.homePage.staffOverview'), label: 'workbench.homePage.staffOverview',
value: WorkCardEnum.PROJECT_MEMBER_VIEW, value: WorkCardEnum.PROJECT_MEMBER_VIEW,
description: t('workbench.homePage.staffOverviewDesc'), description: 'workbench.homePage.staffOverviewDesc',
img: 'staff-overview-img', img: 'staff-overview-img',
}, },
{ {
label: t('workbench.homePage.createdByMe'), label: 'workbench.homePage.createdByMe',
value: WorkCardEnum.CREATE_BY_ME, value: WorkCardEnum.CREATE_BY_ME,
description: t('workbench.homePage.createdByMeDesc'), description: 'workbench.homePage.createdByMeDesc',
img: 'my-created-project-img', img: 'my-created-project-img',
}, },
], ],
}, },
//
{ {
label: t('menu.caseManagement'), label: 'menu.caseManagement',
value: 'caseManagement', value: 'caseManagement',
description: '', description: '',
img: '', img: '',
children: [ children: [
{ {
label: t('workbench.homePage.useCasesNumber'), label: 'workbench.homePage.useCasesNumber',
value: WorkCardEnum.CASE_COUNT, value: WorkCardEnum.CASE_COUNT,
description: t('workbench.homePage.useCasesNumberDesc'), description: 'workbench.homePage.useCasesNumberDesc',
img: 'link-case-img', img: 'link-case-img',
}, },
{ {
label: t('workbench.homePage.useCasesCount'), label: 'workbench.homePage.useCasesCount',
value: WorkCardEnum.ASSOCIATE_CASE_COUNT, value: WorkCardEnum.ASSOCIATE_CASE_COUNT,
description: t('workbench.homePage.useCasesCountDesc'), description: 'workbench.homePage.useCasesCountDesc',
img: 'case-count-img', img: 'case-count-img',
}, },
{ {
label: t('workbench.homePage.numberOfCaseReviews'), label: 'workbench.homePage.numberOfCaseReviews',
value: WorkCardEnum.REVIEW_CASE_COUNT, value: WorkCardEnum.REVIEW_CASE_COUNT,
description: t('workbench.homePage.numberOfCaseReviewsDesc'), description: 'workbench.homePage.numberOfCaseReviewsDesc',
img: 'case-review-img', img: 'case-review-img',
}, },
{ {
label: t('workbench.homePage.waitForReview'), label: 'workbench.homePage.waitForReview',
value: WorkCardEnum.REVIEWING_BY_ME, value: WorkCardEnum.REVIEWING_BY_ME,
description: t('workbench.homePage.waitForReviewDesc'), description: 'workbench.homePage.waitForReviewDesc',
img: 'wait-review-img', img: 'wait-review-img',
}, },
], ],
}, },
//
{ {
label: t('menu.apiTest'), label: 'menu.apiTest',
value: 'apiTest', value: 'apiTest',
description: '', description: '',
img: '', img: '',
children: [ children: [
{ {
label: t('workbench.homePage.apiCount'), label: 'workbench.homePage.apiCount',
value: WorkCardEnum.API_COUNT, value: WorkCardEnum.API_COUNT,
description: t('workbench.homePage.apiCountDesc'), description: 'workbench.homePage.apiCountDesc',
img: 'api-count-img', img: 'api-count-img',
}, },
{ {
label: t('workbench.homePage.apiUseCasesNumber'), label: 'workbench.homePage.apiUseCasesNumber',
value: WorkCardEnum.API_CASE_COUNT, value: WorkCardEnum.API_CASE_COUNT,
description: t('workbench.homePage.apiUseCasesNumberDesc'), description: 'workbench.homePage.apiUseCasesNumberDesc',
img: 'api-use-case-img', img: 'api-use-case-img',
}, },
{ {
label: t('workbench.homePage.scenarioUseCasesNumber'), label: 'workbench.homePage.scenarioUseCasesNumber',
value: WorkCardEnum.SCENARIO_COUNT, value: WorkCardEnum.SCENARIO_COUNT,
description: t('workbench.homePage.scenarioUseCasesNumberDesc'), description: 'workbench.homePage.scenarioUseCasesNumberDesc',
img: 'scenario-case-img', img: 'scenario-case-img',
}, },
{ {
label: t('workbench.homePage.interfaceChange'), label: 'workbench.homePage.interfaceChange',
value: WorkCardEnum.API_CHANGE, value: WorkCardEnum.API_CHANGE,
description: t('workbench.homePage.interfaceChangeDesc'), description: 'workbench.homePage.interfaceChangeDesc',
img: 'api-change-img', img: 'api-change-img',
}, },
], ],
}, },
//
{ {
label: t('menu.testPlan'), label: 'menu.testPlan',
value: 'testPlan', value: 'testPlan',
description: '', description: '',
img: '', img: '',
children: [ children: [
{ {
label: t('workbench.homePage.numberOfTestPlan'), label: 'workbench.homePage.numberOfTestPlan',
value: WorkCardEnum.TEST_PLAN_COUNT, value: WorkCardEnum.TEST_PLAN_COUNT,
description: t('workbench.homePage.numberOfTestPlanDesc'), description: 'workbench.homePage.numberOfTestPlanDesc',
img: 'test-plan-img', img: 'test-plan-img',
}, },
{ {
label: t('workbench.homePage.remainingBugOfPlan'), label: 'workbench.homePage.remainingBugOfPlan',
value: WorkCardEnum.PLAN_LEGACY_BUG, value: WorkCardEnum.PLAN_LEGACY_BUG,
description: t('workbench.homePage.remainingBugOfPlanDesc'), description: 'workbench.homePage.remainingBugOfPlanDesc',
img: 'test-plan-bug-img', img: 'test-plan-bug-img',
}, },
], ],
}, },
//
{ {
label: t('menu.bugManagement'), label: 'menu.bugManagement',
value: 'bugManagement', value: 'bugManagement',
description: '', description: '',
img: '', img: '',
children: [ children: [
{ {
label: t('workbench.homePage.bugCount'), label: 'workbench.homePage.bugCount',
value: WorkCardEnum.BUG_COUNT, value: WorkCardEnum.BUG_COUNT,
description: t('workbench.homePage.bugCountDesc'), description: 'workbench.homePage.bugCountDesc',
img: 'bug-count-img', img: 'bug-count-img',
}, },
{ {
label: t('workbench.homePage.createdBugByMe'), label: 'workbench.homePage.createdBugByMe',
value: WorkCardEnum.CREATE_BUG_BY_ME, value: WorkCardEnum.CREATE_BUG_BY_ME,
description: t('workbench.homePage.createdBugByMeDesc'), description: 'workbench.homePage.createdBugByMeDesc',
img: 'my-created-bug-img', img: 'my-created-bug-img',
}, },
{ {
label: t('workbench.homePage.pendingDefect'), label: 'workbench.homePage.pendingDefect',
value: WorkCardEnum.HANDLE_BUG_BY_ME, value: WorkCardEnum.HANDLE_BUG_BY_ME,
description: t('workbench.homePage.pendingDefectDesc'), description: 'workbench.homePage.pendingDefectDesc',
img: 'wait-handle-bug-img', img: 'wait-handle-bug-img',
}, },
{ {
label: t('workbench.homePage.defectProcessingNumber'), label: 'workbench.homePage.defectProcessingNumber',
value: WorkCardEnum.BUG_HANDLE_USER, value: WorkCardEnum.BUG_HANDLE_USER,
description: t('workbench.homePage.defectProcessingNumberDesc'), description: 'workbench.homePage.defectProcessingNumberDesc',
img: 'bug-handler-img', img: 'bug-handler-img',
}, },
], ],

View File

@ -1,12 +1,11 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper card-min-height">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.useCasesNumber') }} </div> <div class="title"> {{ t('workbench.homePage.useCasesNumber') }} </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -17,17 +16,21 @@
</MsSelect> </MsSelect>
</div> </div>
</div> </div>
<div class="my-[16px]"> <div class="mt-[16px]">
<div class="case-count-wrapper"> <TabCard :content-tab-list="caseCountTabList" not-has-padding hidden-border min-width="270px">
<div class="case-count-item"> <template #item="{ item: tabItem }">
<PassRatePie :options="options" :size="60" :value-list="reviewValueList" /> <div class="w-full">
</div> <PassRatePie
<div class="case-count-item"> :options="tabItem.options"
<PassRatePie :options="options" :size="60" :value-list="passValueList" /> :tooltip-text="tabItem.tooltip"
</div> :size="60"
</div> :value-list="tabItem.valueList"
<div class="mt-[16px]"> />
<SetReportChart size="120px" :legend-data="legendData" :options="executeCharOptions" :request-total="100000" /> </div>
</template>
</TabCard>
<div class="h-[148px]">
<MsChart :options="caseCountOptions" />
</div> </div>
</div> </div>
</div> </div>
@ -38,53 +41,56 @@
* @desc 用例数量 * @desc 用例数量
*/ */
import { ref } from 'vue'; 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 MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue'; 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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const options = ref({ const props = defineProps<{
...commonConfig, item: SelectedCardItem;
tooltip: { }>();
...toolTipConfig,
}, const innerProjectIds = defineModel<string[]>('projectIds', {
legend: { required: true,
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));
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([ const reviewValueList = ref([
{ {
label: t('workbench.homePage.reviewed'), label: t('workbench.homePage.reviewed'),
@ -95,6 +101,7 @@
value: 2000, value: 2000,
}, },
]); ]);
const passValueList = ref([ const passValueList = ref([
{ {
label: t('workbench.homePage.havePassed'), 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));
label: 'P0', const caseCountTabList = computed(() => {
value: 'P0', return [
rote: 30, {
count: 3, label: '',
class: 'bg-[rgb(var(--danger-6))] ml-[24px]', value: 'execution',
}, valueList: reviewValueList.value,
{ options: { ...reviewOptions.value },
label: 'P1', tooltip: 'workbench.homePage.reviewRateTooltip',
value: 'P1', },
rote: 30, {
count: 3, label: '',
class: 'bg-[rgb(var(--warning-6))] ml-[24px]', value: 'pass',
}, valueList: passValueList.value,
{ options: { ...passOptions.value },
label: 'P2', tooltip: 'workbench.homePage.reviewPassRateTooltip',
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',
},
},
],
},
}); });
</script>
<style scoped lang="less"> // X
.card-wrapper { function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
margin: 16px 0; const { review, pass } = statusStatisticsMap;
padding: 24px; reviewValueList.value = handlePassRatePercent(review);
box-shadow: 0 0 10px rgba(120 56 135/ 5%); passValueList.value = handlePassRatePercent(pass);
@apply rounded-xl bg-white;
.title { reviewOptions.value.series.data = handlePassRatePercent(review);
font-size: 16px; passOptions.value.series.data = handlePassRatePercent(pass);
@apply font-medium;
} reviewOptions.value.title.text = review[0].name ?? '';
.case-count-wrapper { reviewOptions.value.title.subtext = `${review[0].count ?? 0}%`;
@apply flex items-center gap-4; passOptions.value.title.text = pass[0].name ?? '';
.case-count-item { passOptions.value.title.subtext = `${pass[0].count ?? 0}%`;
@apply flex-1; 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);
} }
} }
</style>
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"></style>

View File

@ -1,12 +1,11 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper card-min-height">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.numberOfCaseReviews') }} </div> <div class="title"> {{ t('workbench.homePage.numberOfCaseReviews') }} </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -17,14 +16,19 @@
</MsSelect> </MsSelect>
</div> </div>
</div> </div>
<div class="my-[16px]"> <div class="mt-[16px]">
<div class="case-count-wrapper"> <div class="case-count-wrapper mb-[16px]">
<div class="case-count-item"> <div class="case-count-item">
<PassRatePie :options="options" :size="60" :value-list="coverValueList" /> <PassRatePie
:options="options"
tooltip-text="workbench.homePage.caseReviewCoverRateTooltip"
:size="60"
:value-list="coverValueList"
/>
</div> </div>
</div> </div>
<div class="mt-[16px]"> <div class="h-[148px]">
<SetReportChart size="120px" :legend-data="legendData" :options="executeCharOptions" :request-total="100000" /> <MsChart :options="caseReviewCountOptions" />
</div> </div>
</div> </div>
</div> </div>
@ -35,53 +39,43 @@
* @desc 用例评审数量 * @desc 用例评审数量
*/ */
import { ref } from 'vue'; 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 MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue'; 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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 appStore = useAppStore();
const projectIds = ref(''); const props = defineProps<{
const { t } = useI18n(); item: SelectedCardItem;
}>();
const options = ref({ const innerProjectIds = defineModel<string[]>('projectIds', {
...commonConfig, required: true,
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<Record<string, any>>(cloneDeep(commonRatePieOptions));
const coverValueList = ref([ const coverValueList = ref([
{ {
label: t('workbench.homePage.covered'), label: t('workbench.homePage.covered'),
@ -93,83 +87,79 @@
}, },
]); ]);
const legendData = ref<LegendData[]>([ // TODO
{ const detail = ref<PassRateDataType>({
label: t('common.notStarted'), statusStatisticsMap: {
value: 'P0', cover: [
rote: 30, { name: '覆盖率', count: 10 },
count: 3, { name: '已覆盖', count: 2 },
class: 'bg-[rgb(var(--primary-4))] ml-[24px]', { name: '未覆盖', count: 1 },
},
{
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',
},
},
], ],
}, },
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> </script>
<style scoped lang="less"></style> <style scoped lang="less"></style>

View File

@ -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>

View File

@ -1,38 +1,72 @@
<template> <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> </template>
<script setup lang="ts"> <script setup lang="ts">
/** * /** *
* @desc 待我处理的缺陷&缺陷数量 * @desc 用于缺陷数量待我处理的缺陷数量组件
*/ */
import { ref } from 'vue'; 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 { 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 { WorkCardEnum } from '@/enums/workbenchEnum';
import { commonRatePieOptions, handlePieData } from '../utils';
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
type: WorkCardEnum; item: SelectedCardItem;
}>(); }>();
const title = computed(() => { const innerProjectIds = defineModel<string[]>('projectIds', {
switch (props.type) { required: true,
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 projectId = ref<string>(innerProjectIds.value[0]);
const valueList = ref< const valueList = ref<
{ {
label: string; label: string;
@ -49,36 +83,113 @@
}, },
]); ]);
const legendData = ref<LegendData[]>([ const timeForm = inject<Ref<TimeFormParams>>(
{ 'timeForm',
label: t('common.notStarted'), ref({
value: 'notStarted', dayNumber: 3,
rote: 30, startTime: 0,
count: 3, endTime: 0,
class: 'bg-[rgb(var(--primary-4))] ml-[24px]', })
);
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'), deep: true,
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]',
},
]);
</script> </script>
<style scoped lang="less"></style> <style scoped lang="less"></style>

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper">
<div class="flex items-center justify-between"> <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]"> <div class="flex items-center gap-[8px]">
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear allow-clear
allow-search allow-search
@ -30,7 +30,7 @@
</div> </div>
</div> </div>
<div class="mt-[16px]"> <div class="mt-[16px]">
<MsChart height="300px" :options="options" /> <MsChart height="260px" :options="options" />
</div> </div>
</div> </div>
</template> </template>
@ -47,14 +47,38 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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'; import type { SelectOptionData } from '@arco-design/web-vue';
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
item: SelectedCardItem;
}>();
const memberIds = ref(''); const memberIds = ref('');
const projectIds = ref(''); const innerProjectIds = defineModel<string[]>('projectIds', {
const appStore = useAppStore(); 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[]>([]); const memberOptions = ref<SelectOptionData[]>([]);
@ -63,30 +87,139 @@
const hasRoom = computed(() => members.value.length >= 7); const hasRoom = computed(() => members.value.length >= 7);
const seriesData = ref<Record<string, any>[]>([ const seriesData = ref<Record<string, any>[]>([
{ {
name: '已结束', name: '新创建',
type: 'bar', type: 'bar',
barWidth: 12, barWidth: 12,
stack: 'bug',
itemStyle: { itemStyle: {
borderRadius: [2, 2, 0, 0], // borderRadius: [2, 2, 0, 0],
}, },
data: [400, 200, 400, 200, 400, 200], data: [400, 200, 400, 200, 400, 200],
}, },
{ {
name: '未结束', name: '激活',
type: 'bar', type: 'bar',
barWidth: 12, 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: { itemStyle: {
borderRadius: [2, 2, 0, 0], borderRadius: [2, 2, 0, 0],
}, },
data: [90, 160, 90, 160, 90, 160], data: [90, 160, 90, 160, 90, 160],
}, },
]); ]);
const defectStatusColor = ['#811FA3', '#FFA200', '#3370FF', '#F24F4F'];
onMounted(() => { function getDefectMemberDetail() {
options.value = getCommonBarOptions(hasRoom.value, defectStatusColor); options.value = getCommonBarOptions(hasRoom.value, [...defectStatusColor, ...commonColorConfig]);
options.value.xAxis.data = members.value; options.value.xAxis.data = members.value;
options.value.series = seriesData.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> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> {{ props.title }} </div> <div class="title"> {{ t(props.item.label) }} </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="innerProjectIds"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear allow-clear
allow-search allow-search
@ -15,13 +15,14 @@
:prefix="t('workbench.homePage.project')" :prefix="t('workbench.homePage.project')"
:multiple="true" :multiple="true"
:has-all-select="true" :has-all-select="true"
:default-all-select="true" :default-all-select="!(props.item.projectIds || []).length"
:at-least-one="true"
> >
</MsSelect> </MsSelect>
</div> </div>
</div> </div>
<div class="my-[16px]"> <div class="my-[16px]">
<TabCard :content-tab-list="contentTabList" /> <TabCard :content-tab-list="cardModuleList" />
</div> </div>
<!-- 概览图 --> <!-- 概览图 -->
<div> <div>
@ -40,152 +41,136 @@
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import TabCard from './tabCard.vue'; import TabCard from './tabCard.vue';
import { workMyCreatedDetail, workProOverviewDetail } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
title: string; item: SelectedCardItem;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
const projectIds = ref(''); const innerProjectIds = defineModel<string[]>('projectIds', {
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 xAxisType = computed(() => {
return contentTabList.value.map((e) => e.label);
}); });
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 options = ref<Record<string, any>>({});
const seriesData = ref<Record<string, any>[]>([
{ const cardModuleList = ref<ModuleCardItem[]>([]);
name: '项目A',
type: 'bar', function handleData(detail: OverViewOfProject) {
barWidth: 12, //
itemStyle: { const tempAxisData = detail.xaxis.map((xAxisKey) => {
borderRadius: [2, 2, 0, 0], // const data = contentTabList.value.find((e) => e.value === xAxisKey);
}, return {
data: [null, 230, 150, 80, 70, 110, 130], ...data,
}, count: detail.caseCountMap[xAxisKey as WorkOverviewEnum],
{ };
name: '项目B', });
type: 'bar',
barWidth: 12, cardModuleList.value = tempAxisData as ModuleCardItem[];
itemStyle: { options.value = getCommonBarOptions(hasRoom.value, commonColorConfig);
borderRadius: [2, 2, 0, 0], // const { invisible, text } = handleNoDataDisplay(detail.xaxis, detail.projectCountList);
}, options.value.graphic.invisible = invisible;
data: [90, 160, 130, 100, 90, 120, 140], options.value.graphic.style.text = text;
}, // x
{ options.value.xAxis.data = cardModuleList.value.map((e) => e.label);
name: '项目C',
type: 'bar', // data
barWidth: 12, options.value.series = detail.projectCountList.map((item) => {
itemStyle: { return {
borderRadius: [2, 2, 0, 0], // name: item.name,
}, type: 'bar',
data: [100, 140, 120, 90, 100, 130, 120], barWidth: 12,
}, itemStyle: {
{ borderRadius: [2, 2, 0, 0], //
name: '项目D', },
type: 'bar', data: item.count,
barWidth: 12, };
itemStyle: { });
borderRadius: [2, 2, 0, 0], // }
},
data: [90, 160, 130, 100, 90, 120, 140], async function initOverViewDetail() {
}, try {
{ const { startTime, endTime, dayNumber } = timeForm.value;
name: '项目E', const params = {
type: 'bar', current: 1,
barWidth: 12, pageSize: 5,
itemStyle: { startTime: dayNumber ? null : startTime,
borderRadius: [2, 2, 0, 0], // endTime: dayNumber ? null : endTime,
}, dayNumber: dayNumber ?? null,
data: [100, 140, 120, 90, 100, 130, 120], projectIds: innerProjectIds.value,
}, organizationId: appStore.currentOrgId,
{ handleUsers: [],
name: '项目F', };
type: 'bar', let detail;
barWidth: 12, if (props.item.key === WorkCardEnum.PROJECT_VIEW) {
itemStyle: { detail = await workProOverviewDetail(params);
borderRadius: [2, 2, 0, 0], // } else {
}, detail = await workMyCreatedDetail(params);
data: [100, 140, 120, 90, 40, 130, 120], }
},
{ handleData(detail);
name: '项目G', } catch (error) {
type: 'bar', console.log(error);
barWidth: 12, }
itemStyle: { }
borderRadius: [2, 2, 0, 0], //
},
data: [100, 140, 120, 90, 40, 130, 120],
},
]);
onMounted(() => { onMounted(() => {
options.value = getCommonBarOptions(hasRoom.value, commonColorConfig); initOverViewDetail();
options.value.xAxis.data = xAxisType.value;
options.value.series = seriesData.value;
}); });
watch(
() => innerProjectIds.value,
(val) => {
if (val) {
initOverViewDetail();
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
initOverViewDetail();
}
},
{
deep: true,
}
);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -1,12 +1,11 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper">
<div class="flex items-center justify-between"> <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]"> <div class="flex items-center gap-[8px]">
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -18,8 +17,10 @@
<MsSelect <MsSelect
v-model:model-value="memberIds" v-model:model-value="memberIds"
:options="memberOptions" :options="memberOptions"
:allow-search="false" allow-search
allow-clear allow-clear
value-key="value"
label-key="label"
class="!w-[240px]" class="!w-[240px]"
:prefix="t('workbench.homePage.staff')" :prefix="t('workbench.homePage.staff')"
:multiple="true" :multiple="true"
@ -29,11 +30,8 @@
</MsSelect> </MsSelect>
</div> </div>
</div> </div>
<div class="my-[16px]">
<TabCard :content-tab-list="contentTabList" />
</div>
<!-- 概览图 --> <!-- 概览图 -->
<div> <div class="mt-[16px]">
<MsChart height="300px" :options="options" /> <MsChart height="300px" :options="options" />
</div> </div>
</div> </div>
@ -47,233 +45,142 @@
import MsChart from '@/components/pure/chart/index.vue'; import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select'; 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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 { commonColorConfig, contentTabList, getCommonBarOptions, handleNoDataDisplay } from '../utils';
import type { SelectOptionData } from '@arco-design/web-vue';
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
const props = defineProps<{
item: SelectedCardItem;
}>();
const memberIds = ref(''); const innerProjectIds = defineModel<string[]>('projectIds', {
const projectIds = ref(''); required: true,
const memberOptions = ref<SelectOptionData[]>([]);
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 hasRoom = computed(() => projectIds.value.length >= 7);
// TODO
const members = computed(() => ['张三', '李四', '王五', '小王']);
const staticData = [
{
name: '',
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],
itemStyle: {
borderRadius: [2, 2, 0, 0], //
},
},
];
const lastSeriousData = computed(() => {
contentTabList.value.forEach((e, i) => {
staticData[i].name = e.label;
});
return staticData;
}); });
const options = ref({ const innerHandleUsers = defineModel<string[]>('handleUsers', {
tooltip: { required: true,
trigger: 'axis', });
borderWidth: 0,
formatter(params: any) { const projectId = ref<string>(innerProjectIds.value[0]);
const html = `
<div class="w-[186px] h-[auto] p-[16px] flex flex-col"> const memberIds = ref<string[]>(innerHandleUsers.value);
${params
.map( const timeForm = inject<Ref<TimeFormParams>>(
(item: any) => ` 'timeForm',
<div class="flex items-center justify-between mb-2"> ref({
<div class="flex items-center"> dayNumber: 3,
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm" style="background:${item.color}"></div> startTime: 0,
<div style="color:#959598">${item.seriesName}</div> endTime: 0,
</div> })
<div class="text-[#323233] font-medium">${addCommasToNumber(item.value)}</div> );
</div> const hasRoom = computed(() => memberIds.value.length >= 7);
` const memberOptions = ref<{ label: string; value: string }[]>([]);
)
.join('')} const options = ref<Record<string, any>>({});
</div> function handleData(detail: OverViewOfProject) {
`; options.value = getCommonBarOptions(hasRoom.value, commonColorConfig);
return html; const { invisible, text } = handleNoDataDisplay(detail.xaxis, detail.projectCountList);
}, options.value.graphic.invisible = invisible;
}, options.value.graphic.style.text = text;
color: commonColorConfig, options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10));
grid: { options.value.series = detail.projectCountList.map((item, index) => {
top: '36px', return {
left: '10px', name: contentTabList.value[index].label,
right: '10px', type: 'bar',
bottom: hasRoom.value ? '54px' : '5px', stack: 'member',
containLabel: true, barWidth: 12,
}, data: item.count,
xAxis: { itemStyle: {
splitLine: false, borderRadius: [2, 2, 0, 0],
boundaryGap: true,
type: 'category',
data: members.value,
axisLabel: {
color: '#646466',
},
axisTick: {
show: false, // 线
},
axisLine: {
lineStyle: {
color: '#EDEDF1',
}, },
}, };
});
}
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();
}
}, },
yAxis: [ {
{ deep: true,
type: 'value', }
name: '单位:个', // );
nameLocation: 'end',
nameTextStyle: { onMounted(() => {
fontSize: 12, getMemberOptions();
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',
},
{
type: 'slider',
},
]
: [],
}); });
</script> </script>

View File

@ -1,6 +1,16 @@
<template> <template>
<div class="pass-rate-content"> <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 class="pass-rate-title flex-1">
<div v-for="item of props.valueList" :key="item.label" class="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> <div class="mb-[8px] text-[var(--color-text-4)]">{{ item.label }}</div>
@ -23,6 +33,7 @@
const props = defineProps<{ const props = defineProps<{
options: Record<string, any>; options: Record<string, any>;
size: number; size: number;
tooltipText?: string;
valueList: { valueList: {
label: string; label: string;
value: number; value: number;
@ -46,4 +57,9 @@
} }
} }
} }
.tooltip-rate {
position: absolute;
z-index: 9;
border-radius: 50%;
}
</style> </style>

View File

@ -13,10 +13,11 @@
import { addCommasToNumber } from '@/utils'; import { addCommasToNumber } from '@/utils';
const props = defineProps<{ const props = defineProps<{
data: Record<string, any>[]; data: { name: string; value: number }[];
rateConfig: { rateConfig: {
name: string; name: string;
count: string; count: string;
color: string[];
}; };
}>(); }>();
@ -25,7 +26,7 @@
show: true, show: true,
text: '', text: '',
left: 'center', left: 'center',
top: '24px', top: 32,
textStyle: { textStyle: {
fontSize: 12, fontSize: 12,
fontWeight: 'normal', fontWeight: 'normal',
@ -36,6 +37,7 @@
fontSize: 14, fontSize: 14,
color: '#323233', color: '#323233',
fontWeight: 'bold', fontWeight: 'bold',
lineHeight: 3,
}, },
}, },
grid: { grid: {
@ -80,8 +82,10 @@
series: { series: {
name: '', name: '',
type: 'pie', type: 'pie',
padAngle: 1,
radius: ['46%', '56%'], radius: ['46%', '56%'],
center: ['50%', '32%'], center: ['50%', '32%'],
color: [],
avoidLabelOverlap: false, avoidLabelOverlap: false,
label: { label: {
show: false, show: false,
@ -103,16 +107,18 @@
const options = ref({}); const options = ref({});
function initOptions() { function initOptions() {
const { name, count, color } = props.rateConfig;
options.value = { options.value = {
...commonOptionConfig.value, ...commonOptionConfig.value,
title: { title: {
...commonOptionConfig.value.title, ...commonOptionConfig.value.title,
text: props.rateConfig.name, text: name,
subtext: props.rateConfig.count, subtext: count,
}, },
series: { series: {
...commonOptionConfig.value.series, ...commonOptionConfig.value.series,
data: [...props.data], data: [...props.data],
color,
}, },
}; };
} }

View File

@ -1,13 +1,11 @@
<template> <template>
<div class="card-wrapper"> <div :class="`card-wrapper ${props.item.fullScreen ? '' : 'card-min-height'}`">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.useCasesCount') }} </div> <div class="title"> {{ t('workbench.homePage.useCasesCount') }} </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
:search-keys="['name']" :search-keys="['name']"
@ -17,21 +15,17 @@
</MsSelect> </MsSelect>
</div> </div>
</div> </div>
<div class="my-[16px]"> <div class="mt-[16px]">
<div class="case-count-wrapper"> <div class="case-count-wrapper">
<div class="case-count-item"> <div class="case-count-item">
<PassRatePie :options="options" :size="60" :value-list="coverRateValueList" /> <PassRatePie
:options="options"
tooltip-text="workbench.homePage.associateCaseCoverRateTooltip"
:size="60"
:value-list="coverRateValueList"
/>
</div> </div>
</div> </div>
<div class="mt-[16px]">
<SetReportChart
size="120px"
gap="24"
:legend-data="legendData"
:options="executeCharOptions"
:request-total="100000"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -41,128 +35,112 @@
* @desc 关联用例数量 * @desc 关联用例数量
*/ */
import { ref } from 'vue'; import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue'; 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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const projectIds = ref('');
const options = ref({ const props = defineProps<{
...commonConfig, item: SelectedCardItem;
tooltip: { }>();
...toolTipConfig,
}, const innerProjectIds = defineModel<string[]>('projectIds', {
legend: { required: true,
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 coverRateValueList = ref([ const projectId = ref<string>(innerProjectIds.value[0]);
{
label: t('workbench.homePage.covered'),
value: 10000,
},
{
label: t('workbench.homePage.notCover'),
value: 2000,
},
]);
const legendData = ref<LegendData[]>([ const timeForm = inject<Ref<TimeFormParams>>(
{ 'timeForm',
label: t('workbench.homePage.apiUseCases'), ref({
value: 'apiUseCases', dayNumber: 3,
rote: 30, startTime: 0,
count: 3, endTime: 0,
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 options = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
const executeCharOptions = ref({
...commonConfig, const coverRateValueList = ref<{ value: number; label: string; name: string }[]>([]);
tooltip: {
...toolTipConfig, async function getRelatedCaseCount() {
}, try {
series: { const { startTime, endTime, dayNumber } = timeForm.value;
...seriesConfig, const detail = await workAssociateCaseDetail({
data: [ current: 1,
{ pageSize: 5,
value: 0, startTime: dayNumber ? null : startTime,
name: t('common.success'), endTime: dayNumber ? null : endTime,
itemStyle: { dayNumber: dayNumber ?? null,
color: '#00C261', projectIds: innerProjectIds.value,
}, organizationId: appStore.currentOrgId,
}, handleUsers: [],
{ });
value: 0, const { cover } = detail.statusStatisticsMap;
name: t('common.fakeError'), coverRateValueList.value = cover.slice(1).map((item) => {
itemStyle: { return {
color: '#FFC14E', value: item.count,
}, label: item.name,
}, name: item.name,
{ };
value: 0, });
name: t('common.fail'), options.value.series.data = coverRateValueList.value;
itemStyle: { options.value.title.text = cover[0].name ?? '';
color: '#ED0303', options.value.title.subtext = `${cover[0].count ?? 0}%`;
}, options.value.series.color = ['#00C261', '#D4D4D8'];
}, } catch (error) {
{ // eslint-disable-next-line no-console
value: 0, console.log(error);
name: t('common.unExecute'), }
itemStyle: { }
color: '#D4D4D8',
}, onMounted(() => {
}, getRelatedCaseCount();
{
value: 0,
name: t('common.block'),
itemStyle: {
color: '#B379C8',
},
},
],
},
}); });
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> </script>
<style scoped lang="less"></style> <style scoped lang="less"></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div ref="cardWrapperRef">
<a-tabs v-if="props.contentTabList.length" default-active-key="1" class="ms-tab-card"> <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}`"> <a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="`${item.label}`">
<template #title> <template #title>
@ -45,23 +45,28 @@
const width = ref<string | number>(); const width = ref<string | number>();
const cardWrapperRef = ref<HTMLElement | null>(null);
const calculateWidth = debounce(() => { const calculateWidth = debounce(() => {
const wrapperContent = document.querySelector('.card-wrapper') as HTMLElement; const wrapperContent = cardWrapperRef.value as HTMLElement;
if (wrapperContent) { if (wrapperContent) {
const wrapperTotalWidth = wrapperContent.offsetWidth; const wrapperTotalWidth = wrapperContent.offsetWidth;
const gap = 16; const gap = 16;
const paddingNumber = 16;
const paddingWidth = props.notHasPadding ? 0 : paddingNumber * 2; // (16px * 2)
const gapWidth = (props.contentTabList.length - 1) * gap; // const gapWidth = (props.contentTabList.length - 1) * gap; //
const borderWidth = props.hiddenBorder ? 0 : 2; const itemWidth = Math.floor((wrapperTotalWidth - gapWidth) / props.contentTabList.length);
const itemWidth = Math.floor((wrapperTotalWidth - paddingWidth - gapWidth) / props.contentTabList.length); width.value = `${itemWidth}px`;
width.value = `${itemWidth - borderWidth}px`;
} }
}, 300); }, 50);
let resizeObserver: ResizeObserver;
onMounted(() => { onMounted(() => {
calculateWidth(); const wrapperContent = cardWrapperRef.value;
window.addEventListener('resize', calculateWidth); if (wrapperContent) {
resizeObserver = new ResizeObserver(() => {
calculateWidth();
});
resizeObserver.observe(wrapperContent);
}
}); });
const minwidth = ref(); const minwidth = ref();
@ -74,12 +79,22 @@
minwidth.value = `${newMinWidth || '136px'}`; minwidth.value = `${newMinWidth || '136px'}`;
padding.value = `${noPadding ? '0px' : '16px'}`; padding.value = `${noPadding ? '0px' : '16px'}`;
color.value = `${isHiddenBorder ? 'transparent' : 'var(--color-text-n8)'}`; color.value = `${isHiddenBorder ? 'transparent' : 'var(--color-text-n8)'}`;
calculateWidth();
}, },
{ {
immediate: true, immediate: true,
} }
); );
watch(
() => props.contentTabList,
(val) => {
if (val.length) {
calculateWidth();
}
}
);
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', calculateWidth); window.removeEventListener('resize', calculateWidth);
}); });

View File

@ -1,14 +1,13 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper card-min-height">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> <div class="title">
{{ t('workbench.homePage.numberOfTestPlan') }} {{ t('workbench.homePage.numberOfTestPlan') }}
</div> </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -20,15 +19,15 @@
</div> </div>
</div> </div>
<div class="mt-[16px]"> <div class="mt-[16px]">
<TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border> <TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border min-width="270px">
<template #item="{ item }"> <template #item="{ item: tabItem }">
<div class="w-full"> <div class="w-full">
<PassRatePie :options="options" :size="60" :value-list="item.valueList" /> <PassRatePie :options="tabItem.options" :size="60" :value-list="tabItem.valueList" />
</div> </div>
</template> </template>
</TabCard> </TabCard>
<div class="mt-[16px]"> <div class="h-[148px]">
<SetReportChart size="120px" :legend-data="legendData" :options="testPlanCharOptions" :request-total="100000" /> <MsChart :options="testPlanCountOptions" />
</div> </div>
</div> </div>
</div> </div>
@ -39,54 +38,78 @@
* @desc 测试计划数量 * @desc 测试计划数量
*/ */
import { ref } from 'vue'; 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 MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue'; import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.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 { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { LegendData } from '@/models/apiTest/report'; import type {
import { WorkCardEnum } from '@/enums/workbenchEnum'; PassRateDataType,
SelectedCardItem,
StatusStatisticsMapType,
TimeFormParams,
} from '@/models/workbench/homePage';
import { commonRatePieOptions, handlePieData } from '../utils';
const props = defineProps<{
item: SelectedCardItem;
}>();
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
const projectIds = ref(''); 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,
})
);
// 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([ const executionValueList = ref([
{ {
@ -125,106 +148,127 @@
value: 2000, value: 2000,
}, },
]); ]);
const testPlanTabList = computed(() => { const testPlanTabList = computed(() => {
return [ return [
{ {
label: '', label: '',
value: 'execution', value: 'execution',
valueList: executionValueList.value, valueList: executionValueList.value,
options, options: { ...executionOptions.value },
}, },
{ {
label: '', label: '',
value: 'pass', value: 'pass',
valueList: passValueList.value, valueList: passValueList.value,
options, options: { ...passOptions.value },
}, },
{ {
label: '', label: '',
value: 'complete', value: 'complete',
valueList: completeValueList.value, valueList: completeValueList.value,
options, options: { ...completeOptions.value },
}, },
]; ];
}); });
const testPlanCharOptions = ref({ function handlePassRatePercent(data: { name: string; count: number }[]) {
...commonConfig, return data.slice(1).map((item) => {
tooltip: { return {
...toolTipConfig, value: item.count,
}, label: item.name,
series: { name: item.name,
...seriesConfig, };
data: [ });
{ }
value: 0,
name: t('common.success'), function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
itemStyle: { const { execute, pass, complete } = statusStatisticsMap;
color: '#00C261', executionValueList.value = handlePassRatePercent(execute);
}, passValueList.value = handlePassRatePercent(pass);
}, completeValueList.value = handlePassRatePercent(complete);
{
value: 0, executionOptions.value.series.data = handlePassRatePercent(execute);
name: t('common.fakeError'), passOptions.value.series.data = handlePassRatePercent(pass);
itemStyle: { completeOptions.value.series.data = handlePassRatePercent(complete);
color: '#FFC14E',
}, executionOptions.value.title.text = execute[0].name ?? '';
}, executionOptions.value.title.subtext = `${execute[0].count ?? 0}%`;
{
value: 0, passOptions.value.title.text = pass[0].name ?? '';
name: t('common.fail'), passOptions.value.title.subtext = `${pass[0].count ?? 0}%`;
itemStyle: {
color: '#ED0303', completeOptions.value.title.text = complete[0].name ?? '';
}, completeOptions.value.title.subtext = `${complete[0].count ?? 0}%`;
},
{ executionOptions.value.series.color = ['#D4D4D8', '#00C261'];
value: 0, passOptions.value.series.color = ['#D4D4D8', '#00C261'];
name: t('common.unExecute'), completeOptions.value.series.color = ['#00C261', '#3370FF', '#D4D4D8'];
itemStyle: { }
color: '#D4D4D8',
}, const testPlanCountOptions = ref({});
}, async function initTestPlanCount() {
{ try {
value: 0, const { startTime, endTime, dayNumber } = timeForm.value;
name: t('common.block'), const params = {
itemStyle: { current: 1,
color: '#B379C8', 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[]>([ onMounted(() => {
{ initTestPlanCount();
label: t('common.notStarted'), });
value: 'notStarted',
rote: 30, watch(
count: 3, () => projectId.value,
class: 'bg-[rgb(var(--primary-4))] ml-[24px]', (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'), deep: true,
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]',
},
]);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -1,12 +1,11 @@
<template> <template>
<div class="card-wrapper"> <div class="card-wrapper">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.waitForReview') }} </div> <div class="title"> {{ t(props.item.label) }} </div>
<div> <div>
<MsSelect <MsSelect
v-model:model-value="projectIds" v-model:model-value="projectId"
:options="appStore.projectList" :options="appStore.projectList"
allow-clear
allow-search allow-search
value-key="id" value-key="id"
label-key="name" label-key="name"
@ -78,13 +77,30 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 appStore = useAppStore();
const { t } = useI18n(); 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 = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
@ -122,10 +138,55 @@
showSelectAll: false, showSelectAll: false,
}); });
onMounted(() => { function initData() {
setLoadListParams({}); 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(); 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> </script>
<style scoped></style> <style scoped></style>

View File

@ -9,18 +9,18 @@
size="medium" size="medium"
@change="handleChangeTime" @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') }} {{ t('workbench.homePage.nearlyThreeDays') }}
</a-radio> </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') }} {{ t('workbench.homePage.nearlySevenDays') }}
</a-radio> </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') }} {{ t('workbench.homePage.customize') }}
</a-radio> </a-radio>
</a-radio-group> </a-radio-group>
<a-range-picker <a-range-picker
v-if="timeForm.dayNumber === 'customize'" v-if="!timeForm.dayNumber"
v-model:model-value="rangeTime" v-model:model-value="rangeTime"
show-time show-time
value-format="timestamp" value-format="timestamp"
@ -28,43 +28,96 @@
defaultValue: tempRange, defaultValue: tempRange,
}" }"
class="w-[360px]" class="w-[360px]"
@select="handleTimeSelect" @ok="handleTimeSelect"
/> />
</div> </div>
<a-button type="outline" class="arco-btn-outline--secondary !px-[8px]" @click="cardSetting"> <div class="flex items-center gap-[8px]">
<template #icon> <MsTag
<icon-settings class="setting-icon" @click="handleShowSetting" /> no-margin
</template> :tooltip-disabled="true"
{{ t('workbench.homePage.cardSetting') }} class="h-[30px] cursor-pointer"
</a-button> 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" />
</template>
{{ t('workbench.homePage.cardSetting') }}
</a-button>
</div>
</div> </div>
</div> </div>
<!-- TODO 等待卡片设置列表排版出来调整 --> <div v-if="defaultWorkList.length" class="card-content mt-[12px] grid grid-cols-2 gap-4">
<div v-if="defaultWorkList.length" class="card-content grid grid-cols-2 gap-4">
<div v-for="item of defaultWorkList" :key="item.id" :class="`card-item ${item.fullScreen ? 'col-span-2' : ''}`"> <div v-for="item of defaultWorkList" :key="item.id" :class="`card-item ${item.fullScreen ? 'col-span-2' : ''}`">
<Overview <Overview
v-if="[WorkCardEnum.CREATE_BY_ME, WorkCardEnum.PROJECT_VIEW].includes(item.key)" 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 <ApiAndScenarioCase
v-else-if="[WorkCardEnum.API_CASE_COUNT, WorkCardEnum.SCENARIO_COUNT].includes(item.key)" 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" :type="item.key"
/> />
<ApiChangeList v-else-if="item.key === WorkCardEnum.API_CHANGE" /> <ApiCount v-else-if="item.key === WorkCardEnum.API_COUNT" v-model:projectIds="item.projectIds" :item="item" />
<DefectMemberBar v-else-if="item.key === WorkCardEnum.BUG_HANDLE_USER" /> <TestPlanCount
<DefectCount v-else-if="countOfBug.includes(item.key)" :type="item.key" /> v-else-if="item.key === WorkCardEnum.TEST_PLAN_COUNT"
<ApiCount v-else-if="item.key === WorkCardEnum.API_COUNT" /> v-model:projectIds="item.projectIds"
<TestPlanCount v-else-if="item.key === WorkCardEnum.TEST_PLAN_COUNT" /> :item="item"
/>
</div> </div>
</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> </div>
<CardSettingDrawer v-model:visible="showSettingDrawer" /> <CardSettingDrawer v-model:visible="showSettingDrawer" :list="defaultWorkList" @success="initDefaultList" />
<MsBackButton target=".page-content" /> <MsBackButton target=".page-content" />
</template> </template>
@ -72,6 +125,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import MsBackButton from '@/components/pure/ms-back-button/index.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 NoData from '../components/notData.vue';
import ApiAndScenarioCase from './components/apiAndScenarioCase.vue'; import ApiAndScenarioCase from './components/apiAndScenarioCase.vue';
import ApiChangeList from './components/apiChangeList.vue'; import ApiChangeList from './components/apiChangeList.vue';
@ -87,9 +141,10 @@
import DefectMemberBar from '@/views/workbench/homePage/components/defectMemberBar.vue'; import DefectMemberBar from '@/views/workbench/homePage/components/defectMemberBar.vue';
import OverviewMember from '@/views/workbench/homePage/components/overviewMember.vue'; import OverviewMember from '@/views/workbench/homePage/components/overviewMember.vue';
import { getDashboardLayout } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store'; import { useUserStore } from '@/store';
import { getGenerateId } from '@/utils'; import useAppStore from '@/store/modules/app';
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage'; import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
import { SelectedCardItem } from '@/models/workbench/homePage'; import { SelectedCardItem } from '@/models/workbench/homePage';
@ -97,15 +152,14 @@
const userStore = useUserStore(); const userStore = useUserStore();
const { t } = useI18n(); const appStore = useAppStore();
// const { t } = useI18n();
function handleShowSetting() {}
const rangeTime = ref<number[]>([]); const rangeTime = ref<number[]>([]);
const tempRange = ref<(Date | string | number)[]>(['00:00:00', '00:00:00']); const tempRange = ref<(Date | string | number)[]>(['00:00:00', '00:00:00']);
const initTime = { const initTime = {
dayNumber: '3', dayNumber: 3,
startTime: 0, startTime: 0,
endTime: 0, endTime: 0,
}; };
@ -119,7 +173,7 @@
// //
function handleChangeTime(value: string | number | boolean, ev: Event) { function handleChangeTime(value: string | number | boolean, ev: Event) {
resetTime(); resetTime();
timeForm.value.dayNumber = value as string; timeForm.value.dayNumber = value as number;
setLocalStorage(`WORK_TIME_${userStore.id}`, JSON.stringify(timeForm.value)); setLocalStorage(`WORK_TIME_${userStore.id}`, JSON.stringify(timeForm.value));
} }
// //
@ -151,31 +205,28 @@
const defaultWorkList = ref<SelectedCardItem[]>([]); const defaultWorkList = ref<SelectedCardItem[]>([]);
function initDefaultList() { const showNoData = ref(false);
if (userStore.isAdmin) { async function initDefaultList() {
defaultWorkList.value = [ try {
{ appStore.showLoading();
id: getGenerateId(), const result = await getDashboardLayout(appStore.currentOrgId);
label: t('workbench.homePage.projectOverview'), defaultWorkList.value = result;
key: WorkCardEnum.PROJECT_VIEW, } catch (error) {
fullScreen: true, // eslint-disable-next-line no-console
isDisabledHalfScreen: true, console.log(error);
projectIds: [], } finally {
handleUsers: [], if (!defaultWorkList.value.length) {
}, showNoData.value = true;
{ }
id: getGenerateId(), appStore.hideLoading();
label: t('workbench.homePage.staffOverview'),
key: WorkCardEnum.PROJECT_MEMBER_VIEW,
fullScreen: true,
isDisabledHalfScreen: true,
projectIds: [],
handleUsers: [],
},
];
} }
} }
//
function handleRefresh() {
initDefaultList();
}
onMounted(() => { onMounted(() => {
initDefaultList(); initDefaultList();
const defaultTime = getLocalStorage(`WORK_TIME_${userStore.id}`); const defaultTime = getLocalStorage(`WORK_TIME_${userStore.id}`);
@ -187,6 +238,8 @@
rangeTime.value = [startTime, endTime]; rangeTime.value = [startTime, endTime];
} }
}); });
provide('timeForm', timeForm);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -194,12 +247,11 @@
.header-setting { .header-setting {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 999; z-index: 9;
background: var(--color-text-n9); background: var(--color-text-n9);
.setting { .setting {
.setting-icon { .setting-icon {
color: var(--color-text-4); color: var(--color-text-1);
background-color: var(--color-text-10);
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: rgba(var(--primary-5)); color: rgba(var(--primary-5));
@ -212,10 +264,12 @@
<style lang="less"> <style lang="less">
.card-wrapper { .card-wrapper {
margin: 16px 0;
padding: 24px; padding: 24px;
box-shadow: 0 0 10px rgba(120 56 135/ 5%); box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply rounded-xl bg-white; @apply rounded-xl bg-white;
&.card-min-height {
min-height: 356px;
}
.title { .title {
font-size: 16px; font-size: 16px;
@apply font-medium; @apply font-medium;

View File

@ -79,4 +79,31 @@ export default {
'workbench.homePage.fullScreen': 'Full screen', 'workbench.homePage.fullScreen': 'Full screen',
'workbench.homePage.sort': 'Sort', 'workbench.homePage.sort': 'Sort',
'workbench.homePage.workNoProjectTip': 'Workbench no content, please join the project', '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%',
}; };

View File

@ -9,7 +9,7 @@ export default {
'workbench.homePage.interfaceCASE': '接口 CASE', 'workbench.homePage.interfaceCASE': '接口 CASE',
'workbench.homePage.interfaceScenario': '接口场景', 'workbench.homePage.interfaceScenario': '接口场景',
'workbench.homePage.apiPlan': '接口计划', 'workbench.homePage.apiPlan': '接口计划',
'workbench.homePage.bugCount': '缺陷数', 'workbench.homePage.bugCount': '缺陷数',
'workbench.homePage.nearlyThreeDays': '近3天', 'workbench.homePage.nearlyThreeDays': '近3天',
'workbench.homePage.nearlySevenDays': '近7天', 'workbench.homePage.nearlySevenDays': '近7天',
'workbench.homePage.customize': '自定义', 'workbench.homePage.customize': '自定义',
@ -18,8 +18,8 @@ export default {
'workbench.homePage.staff': '人员', 'workbench.homePage.staff': '人员',
'workbench.homePage.workEmptyConfig': '工作台暂无内容,立即', 'workbench.homePage.workEmptyConfig': '工作台暂无内容,立即',
'workbench.homePage.configureWorkbench': '配置工作台', 'workbench.homePage.configureWorkbench': '配置工作台',
'workbench.homePage.useCasesCount': '关联用例数', 'workbench.homePage.useCasesCount': '关联用例数',
'workbench.homePage.useCasesNumber': '用例数', 'workbench.homePage.useCasesNumber': '用例数',
'workbench.homePage.reviewed': '已评审', 'workbench.homePage.reviewed': '已评审',
'workbench.homePage.unReviewed': '未评审', 'workbench.homePage.unReviewed': '未评审',
'workbench.homePage.havePassed': '已通过', 'workbench.homePage.havePassed': '已通过',
@ -30,27 +30,27 @@ export default {
'workbench.homePage.sceneUseCase': '场景用例', 'workbench.homePage.sceneUseCase': '场景用例',
'workbench.homePage.waitForReview': '待我评审', 'workbench.homePage.waitForReview': '待我评审',
'workbench.homePage.executionTimes': '执行次数', 'workbench.homePage.executionTimes': '执行次数',
'workbench.homePage.apiUseCasesNumber': '接口用例数', 'workbench.homePage.apiUseCasesNumber': '接口用例数',
'workbench.homePage.misstatementCount': '误报数', 'workbench.homePage.misstatementCount': '误报数',
'workbench.homePage.apiCoverage': '接口覆盖率', 'workbench.homePage.apiCoverage': '接口覆盖率',
'workbench.homePage.caseExecutionRate': '用例执行率', 'workbench.homePage.caseExecutionRate': '用例执行率',
'workbench.homePage.casePassedRate': '用例通过率', 'workbench.homePage.casePassedRate': '用例通过率',
'workbench.homePage.sceneExecutionRate': '场景执行率', 'workbench.homePage.sceneExecutionRate': '场景执行率',
'workbench.homePage.executionRate': '执行通过率', 'workbench.homePage.executionRate': '执行通过率',
'workbench.homePage.scenarioUseCasesNumber': '场景用例数', 'workbench.homePage.scenarioUseCasesNumber': '场景用例数',
'workbench.homePage.interfaceChange': '接口变更', 'workbench.homePage.interfaceChange': '接口变更',
'workbench.homePage.associationCASE': '关联CASE', 'workbench.homePage.associationCASE': '关联CASE',
'workbench.homePage.associatedScene': '关联场景', 'workbench.homePage.associatedScene': '关联场景',
'workbench.homePage.pendingDefect': '待我处理的缺陷', 'workbench.homePage.pendingDefect': '待我处理的缺陷',
'workbench.homePage.defectProcessingNumber': '缺陷处理人数', 'workbench.homePage.defectProcessingNumber': '缺陷处理人数',
'workbench.homePage.defectTotal': '缺陷总数', 'workbench.homePage.defectTotal': '缺陷总数',
'workbench.homePage.legacyDefectsNumber': '遗留缺陷数', 'workbench.homePage.legacyDefectsNumber': '遗留缺陷数',
'workbench.homePage.createdBugByMe': '我创建的缺陷', 'workbench.homePage.createdBugByMe': '我创建的缺陷',
'workbench.homePage.remainingBugOfPlan': '计划遗留缺陷数', 'workbench.homePage.remainingBugOfPlan': '计划遗留缺陷数',
'workbench.homePage.apiCount': '接口数', 'workbench.homePage.apiCount': '接口数',
'workbench.homePage.unFinish': '未完成', 'workbench.homePage.unFinish': '未完成',
'workbench.homePage.numberOfTestPlan': '测试计划数', 'workbench.homePage.numberOfTestPlan': '测试计划数',
'workbench.homePage.numberOfCaseReviews': '用例评审数', 'workbench.homePage.numberOfCaseReviews': '用例评审数',
'workbench.homePage.projectOverview': '项目概览', 'workbench.homePage.projectOverview': '项目概览',
'workbench.homePage.projectOverviewDesc': '统计所在项目的资源及分布的数据统计', 'workbench.homePage.projectOverviewDesc': '统计所在项目的资源及分布的数据统计',
'workbench.homePage.staffOverviewDesc': '统计成员在所选项目中创建的资源及分布的数据统计', 'workbench.homePage.staffOverviewDesc': '统计成员在所选项目中创建的资源及分布的数据统计',
@ -77,4 +77,19 @@ export default {
'workbench.homePage.fullScreen': '全屏', 'workbench.homePage.fullScreen': '全屏',
'workbench.homePage.sort': '排序', 'workbench.homePage.sort': '排序',
'workbench.homePage.workNoProjectTip': '工作台暂无内容,请先加入项目', '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%',
}; };

View File

@ -1,5 +1,12 @@
import { commonConfig, toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils'; import { addCommasToNumber } from '@/utils';
import type { ModuleCardItem } from '@/models/workbench/homePage';
import { WorkCardEnum, WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
const { t } = useI18n();
// 通用颜色配置
export const commonColorConfig = [ export const commonColorConfig = [
'#811FA3', '#811FA3',
'#00C261', '#00C261',
@ -26,28 +33,71 @@ export const commonColorConfig = [
'#87F578', '#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> { export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<string, any> {
return { return {
tooltip: { tooltip: [
trigger: 'item', {
borderWidth: 0, trigger: 'axis',
formatter(params: any) { borderWidth: 0,
const html = ` padding: 0,
<div class="w-[186px] h-[50px] p-[16px] flex items-center justify-between"> label: {
<div class=" flex items-center"> width: 50,
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm bg-[${params.color}]" style="background:${ overflow: 'truncate',
params.color },
}"></div> displayMode: 'single',
<div style="color:#959598">${params.name}</div> enterable: true,
</div> // TODO 单例模式
<div class="text-[#323233] font-medium">${addCommasToNumber(params.value)}</div> // formatter(params: any) {
</div> // 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] 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" 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(item.value)}</div>
</div>
`
)
.join('')}
</div>
`; `;
return html; return html;
},
}, },
}, ],
color, color,
grid: { grid: {
top: '36px', top: '36px',
@ -63,6 +113,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
data: [], data: [],
axisLabel: { axisLabel: {
color: '#646466', color: '#646466',
interval: 0,
}, },
axisTick: { axisTick: {
show: false, // 隐藏刻度线 show: false, // 隐藏刻度线
@ -81,7 +132,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
nameTextStyle: { nameTextStyle: {
fontSize: 12, fontSize: 12,
color: '#AEAEB2', // 自定义字体大小和颜色 color: '#AEAEB2', // 自定义字体大小和颜色
padding: [0, 0, 0, -20], // 通过左侧(最后一个值)的负偏移向左移动 padding: [0, 0, 0, 10], // 通过padding控制Y轴单位距离左侧的距离
}, },
nameGap: 20, nameGap: 20,
splitLine: { 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', colorBy: 'series',
series: [], series: [],
barCategoryGap: '50%', // 控制 X 轴分布居中效果
legend: { legend: {
width: '60%',
show: true, show: true,
type: 'scroll', type: 'scroll',
itemGap: 20, itemGap: 20,
itemWidth: 8, itemWidth: 8,
itemHeight: 8, itemHeight: 8,
left: 'center',
pageButtonItemGap: 5,
pageButtonGap: 5,
pageIconColor: '#00000099',
pageIconInactiveColor: '#00000042',
pageIconSize: [10, 8],
pageTextStyle: {
color: '#00000099',
fontSize: 12,
},
}, },
dataZoom: hasRoom 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;
}

View File

@ -26,20 +26,20 @@
v-if="features.includes(FeatureEnum.TEST_PLAN)" v-if="features.includes(FeatureEnum.TEST_PLAN)"
:project="currentProject" :project="currentProject"
:refresh-id="refreshId" :refresh-id="refreshId"
type="my_follow" type="my_todo"
hide-show-type hide-show-type
/> />
<caseReviewTable <caseReviewTable
v-if="features.includes(FeatureEnum.CASE_REVIEW)" v-if="features.includes(FeatureEnum.CASE_REVIEW)"
:project="currentProject" :project="currentProject"
:refresh-id="refreshId" :refresh-id="refreshId"
type="my_follow" type="my_todo"
/> />
<bugTable <bugTable
v-if="features.includes(FeatureEnum.BUG)" v-if="features.includes(FeatureEnum.BUG)"
:project="currentProject" :project="currentProject"
:refresh-id="refreshId" :refresh-id="refreshId"
type="my_follow" type="my_todo"
/> />
</template> </template>
<NoData v-else all-screen /> <NoData v-else all-screen />