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 { CommonList, TableQueryParams } from '@/models/common';
import type { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan';
import type {
OverViewOfProject,
PassRateDataType,
SelectedCardItem,
WorkHomePageDetail,
} from '@/models/workbench/homePage';
import {
EditDashboardLayoutUrl,
GetDashboardLayoutUrl,
WorkAssociateCaseDetailUrl,
WorkbenchApiCaseListUrl,
WorkbenchBugListUrl,
WorkbenchCaseListUrl,
@ -16,6 +25,13 @@ import {
WorkbenchScenarioListUrl,
WorkbenchTestPlanListUrl,
WorkbenchTestPlanStatisticUrl,
WorkCaseCountDetailUrl,
WorkMemberViewDetailUrl,
WorkMyCreatedDetailUrl,
WorkProOverviewDetailUrl,
WorkTodoBugListUrl,
WorkTodoPlanListUrl,
WorkTodoReviewListUrl,
} from '../requrls/workbench';
// 我的-场景列表
@ -52,3 +68,49 @@ export function workbenchBugList(data: TableQueryParams) {
export function workbenchApiCaseList(data: TableQueryParams) {
return MSR.post<CommonList<ApiCaseDetail>>({ url: WorkbenchApiCaseListUrl, data });
}
// 工作台首页概览
export function workProOverviewDetail(data: WorkHomePageDetail) {
return MSR.post<OverViewOfProject>({ url: WorkProOverviewDetailUrl, data });
}
// 我创建的
export function workMyCreatedDetail(data: WorkHomePageDetail) {
return MSR.post<OverViewOfProject>({ url: WorkMyCreatedDetailUrl, data });
}
// 人员概览
export function workMemberViewDetail(data: WorkHomePageDetail) {
return MSR.post<OverViewOfProject>({ url: WorkMemberViewDetailUrl, data });
}
// 获取用户布局
export function getDashboardLayout(orgId: string) {
return MSR.get<SelectedCardItem[]>({ url: `${GetDashboardLayoutUrl}/${orgId}` });
}
// 获取用户布局
export function editDashboardLayout(data: SelectedCardItem[], orgId: string) {
return MSR.post({ url: `${EditDashboardLayoutUrl}/${orgId}`, data });
}
// 工作台-首页-用例数
export function workCaseCountDetail(data: WorkHomePageDetail) {
return MSR.post<PassRateDataType>({ url: WorkCaseCountDetailUrl, data });
}
// 工作台-首页-关联用例数
export function workAssociateCaseDetail(data: WorkHomePageDetail) {
return MSR.post<PassRateDataType>({ url: WorkAssociateCaseDetailUrl, data });
}
// 待办-用例评审列表
export function workbenchTodoReviewList(data: TableQueryParams) {
return MSR.post<CommonList<ReviewItem>>({ url: WorkTodoReviewListUrl, data });
}
// 待办-缺陷列表
export function workbenchTodoBugList(data: TableQueryParams) {
return MSR.post<CommonList<BugListItem>>({ url: WorkTodoBugListUrl, data });
}
// 待办-测试计划列表
export function workbenchTodoTestPlanList(data: TableQueryParams) {
return MSR.post<CommonList<TestPlanItem>>({ url: WorkTodoPlanListUrl, data });
}

View File

@ -5,3 +5,13 @@ export const WorkbenchTestPlanStatisticUrl = '/dashboard/my/plan/statistics'; //
export const WorkbenchCaseListUrl = '/dashboard/my/functional/page'; // 工作台-我的-用例列表
export const WorkbenchBugListUrl = '/dashboard/my/bug/page'; // 工作台-我的-缺陷列表
export const WorkbenchApiCaseListUrl = '/dashboard/my/api/page'; // 工作台-我的-接口用例列表
export const WorkProOverviewDetailUrl = '/dashboard/project_view'; // 工作台首页项目概览
export const WorkMyCreatedDetailUrl = '/dashboard/create_by_me'; // 工作台我创建的
export const GetDashboardLayoutUrl = '/dashboard/layout/get'; // 获取用户布局
export const EditDashboardLayoutUrl = '/dashboard/layout/edit'; // 更新用户布局
export const WorkTodoPlanListUrl = '/dashboard/todo/plan/page'; // 工作台-待办-测试计划列表
export const WorkTodoReviewListUrl = '/dashboard/todo/review/page'; // 工作台-待办-用例评审
export const WorkTodoBugListUrl = '/dashboard/todo/bug/page'; // 工作台-待办-缺陷列表
export const WorkMemberViewDetailUrl = '/dashboard/project_member_view'; // 工作台-首页-人员概览
export const WorkCaseCountDetailUrl = '/dashboard/case_count'; // 工作台-首页-用例数量
export const WorkAssociateCaseDetailUrl = '/dashboard/associate_case_count'; // 工作台-首页-关联用例数量

View File

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

View File

@ -1,3 +1,4 @@
import { TableQueryParams } from '@/models/common';
import { WorkCardEnum } from '@/enums/workbenchEnum';
// 配置卡片列表
@ -27,3 +28,55 @@ export interface SelectedCardItem {
projectIds: string[];
handleUsers: string[];
}
// 查询入参
export interface WorkHomePageDetail extends TableQueryParams {
dayNumber: number | null;
startTime: number | null;
endTime: number | null;
projectIds: string[];
handleUsers?: string[];
organizationId: string;
}
export interface TimeFormParams {
dayNumber: number | null;
startTime: number | null;
endTime: number | null;
}
export interface OverViewOfProject {
caseCountMap: Record<string, number>; // 模块列表
projectCountList: {
id: string;
name: string;
count: number[];
}[]; // 项目列表
xaxis: string[]; // 横坐标
}
export interface ModuleCardItem {
label: string | number;
value: string | number;
count?: number;
icon?: string;
color?: string;
[key: string]: any;
}
export type StatusStatisticsMapType = Record<
string,
{
name: string;
count: number;
}[]
>;
export interface PassRateDataType {
statusStatisticsMap: StatusStatisticsMapType;
statusPercentList: {
status: string; // 状态
count: number;
percentValue: string; // 百分比
}[];
}

View File

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

View File

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

View File

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

View File

@ -68,7 +68,7 @@
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
import { workbenchReviewList } from '@/api/modules/workbench';
import { workbenchReviewList, workbenchTodoReviewList } from '@/api/modules/workbench';
import { reviewStatusMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
@ -156,7 +156,11 @@
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workbenchReviewList, {
const workbenchReviewPage = computed(() => {
return props.type === 'my_todo' ? workbenchTodoReviewList : workbenchReviewList;
});
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workbenchReviewPage.value, {
columns,
scroll: { x: '100%' },
showSetting: false,
@ -179,6 +183,7 @@
setLoadListParams({
projectId: props.project,
viewId: props.type,
myTodo: props.type === 'my_todo',
});
loadList();
}

View File

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

View File

@ -129,7 +129,11 @@
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
import StatusProgress from '@/views/test-plan/testPlan/components/statusProgress.vue';
import { workbenchTestPlanList, workbenchTestPlanStatistic } from '@/api/modules/workbench';
import {
workbenchTestPlanList,
workbenchTestPlanStatistic,
workbenchTodoTestPlanList,
} from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
@ -246,7 +250,11 @@
showSelectorAll: false,
});
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workbenchTestPlanList, tableProps.value);
const getTestPlanList = computed(() => {
return props.type === 'my_todo' ? workbenchTodoTestPlanList : workbenchTestPlanList;
});
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getTestPlanList.value, tableProps.value);
const planData = computed(() => {
return propsRes.value.data;
@ -293,6 +301,7 @@
type: showType.value,
projectId: props.project,
viewId: props.type,
myTodo: props.type === 'my_todo',
});
loadList();
}

View File

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

View File

@ -1,14 +1,13 @@
<template>
<div class="card-wrapper">
<div class="card-wrapper card-min-height">
<div class="flex items-center justify-between">
<div class="title">
{{ t('workbench.homePage.interfaceChange') }}
{{ t(props.item.label) }}
</div>
<div>
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
value-key="id"
label-key="name"
@ -53,12 +52,29 @@
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
const { t } = useI18n();
const projectIds = ref('');
const appStore = useAppStore();
const props = defineProps<{
item: SelectedCardItem;
}>();
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const columns: MsTableColumn = [
{
title: 'ID',
@ -119,11 +135,57 @@
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
})
);
function initData() {
const { startTime, endTime, dayNumber } = timeForm.value;
setLoadListParams({
current: 1,
pageSize: 5,
startTime: dayNumber ? null : startTime,
endTime: dayNumber ? null : endTime,
dayNumber: dayNumber ?? null,
projectIds: innerProjectIds.value,
organizationId: appStore.currentOrgId,
handleUsers: [],
});
loadList();
}
onMounted(() => {
setLoadListParams({});
loadList();
initData();
});
watch(
() => innerProjectIds.value,
(val) => {
if (val) {
const [newProjectId] = val;
projectId.value = newProjectId;
initData();
}
}
);
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
initData();
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
initData();
}
},
{
deep: true,
}
);
</script>
<style scoped></style>

View File

@ -1,12 +1,11 @@
<template>
<div class="card-wrapper api-count-wrapper">
<div class="card-wrapper api-count-wrapper card-min-height">
<div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.apiCount') }} </div>
<div>
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
value-key="id"
label-key="name"
@ -18,15 +17,19 @@
</div>
</div>
<div class="my-[16px]">
<div class="case-count-wrapper">
<div class="case-count-item">
<PassRatePie :options="options" :size="60" :value-list="coverValueList" />
</div>
<div class="case-count-item">
<PassRatePie :options="options" :size="60" :value-list="passValueList" />
</div>
</div>
<div class="mt-[16px] h-[148px]">
<TabCard :content-tab-list="apiCountTabList" not-has-padding hidden-border min-width="270px">
<template #item="{ item: tabItem }">
<div class="w-full">
<PassRatePie
:tooltip-text="tabItem.tooltip"
:options="tabItem.options"
:size="60"
:value-list="tabItem.valueList"
/>
</div>
</template>
</TabCard>
<div class="h-[148px]">
<MsChart :options="apiCountOptions" />
</div>
</div>
@ -38,53 +41,68 @@
* @desc 接口数量
*/
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.vue';
import { commonConfig, toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { addCommasToNumber } from '@/utils';
import type { SelectOptionData } from '@arco-design/web-vue';
import type {
PassRateDataType,
SelectedCardItem,
StatusStatisticsMapType,
TimeFormParams,
} from '@/models/workbench/homePage';
import { commonRatePieOptions, handlePieData } from '../utils';
const props = defineProps<{
item: SelectedCardItem;
projectIds: string[];
}>();
const { t } = useI18n();
const appStore = useAppStore();
const projectIds = ref('');
const projectOptions = ref<SelectOptionData[]>([]);
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const options = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['80%', '100%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [],
const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const options = ref(cloneDeep(commonRatePieOptions));
// TODO
const detail = ref<PassRateDataType>({
statusStatisticsMap: {
cover: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
success: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
},
statusPercentList: [
{ status: 'HTTP', count: 1, percentValue: '10%' },
{ status: 'TCP', count: 3, percentValue: '0%' },
{ status: 'BBB', count: 6, percentValue: '0%' },
],
});
const coverValueList = ref([
@ -111,148 +129,114 @@
value: 2000,
},
]);
const apiCountOptions = ref({
title: {
show: true,
text: '总数(个)',
left: 60,
top: '38px',
textStyle: {
fontSize: 12,
fontWeight: 'normal',
color: '#959598',
},
subtext: '100111',
subtextStyle: {
fontSize: 20,
color: '#323233',
fontWeight: 'bold',
align: 'center',
},
},
color: ['#811FA3', '#00C261', '#3370FF', '#FFA1FF', '#EE50A3', '#FF9964', '#F9F871', '#C3DD40'],
tooltip: {
...toolTipConfig,
position: 'right',
},
legend: {
width: '100%',
height: 128,
type: 'scroll',
orient: 'vertical',
pageButtonItemGap: 5,
pageButtonGap: 5,
pageIconColor: '#00000099',
pageIconInactiveColor: '#00000042',
pageIconSize: [7, 5],
pageTextStyle: {
color: '#00000099',
fontSize: 12,
},
pageButtonPosition: 'end',
itemGap: 16,
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
bottom: 'center',
left: 180,
formatter: (name: any) => {
return `{a|${name}} {b|${addCommasToNumber(1022220)}} {c|${10}}`;
},
textStyle: {
color: '#333',
fontSize: 14, //
textBorderType: 'solid',
rich: {
a: {
width: 50,
color: '#959598',
fontSize: 12,
align: 'left',
},
b: {
width: 50,
color: '#323233',
fontSize: 12,
fontWeight: 'bold',
align: 'right',
},
c: {
width: 50,
color: '#323233',
fontSize: 12,
fontWeight: 'bold',
align: 'right',
},
},
},
},
media: [
const coverOptions = ref<Record<string, any>>(cloneDeep(options.value));
const completeOptions = ref<Record<string, any>>(cloneDeep(options.value));
const apiCountTabList = computed(() => {
return [
{
query: { maxWidth: 600 },
option: {
legend: {
textStyle: {
width: 200,
},
},
},
label: '',
value: 'execution',
valueList: coverValueList.value,
options: { ...coverOptions.value },
tooltip: 'workbench.homePage.apiCountCoverRateTooltip',
},
{
query: { minWidth: 601, maxWidth: 800 },
option: {
legend: {
textStyle: {
width: 450,
},
},
},
label: '',
value: 'pass',
valueList: passValueList.value,
options: { ...completeOptions.value },
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>
<style scoped lang="less">

View File

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

View File

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

View File

@ -1,12 +1,11 @@
<template>
<div class="card-wrapper">
<div class="card-wrapper card-min-height">
<div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.useCasesNumber') }} </div>
<div>
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
value-key="id"
label-key="name"
@ -17,17 +16,21 @@
</MsSelect>
</div>
</div>
<div class="my-[16px]">
<div class="case-count-wrapper">
<div class="case-count-item">
<PassRatePie :options="options" :size="60" :value-list="reviewValueList" />
</div>
<div class="case-count-item">
<PassRatePie :options="options" :size="60" :value-list="passValueList" />
</div>
</div>
<div class="mt-[16px]">
<SetReportChart size="120px" :legend-data="legendData" :options="executeCharOptions" :request-total="100000" />
<div class="mt-[16px]">
<TabCard :content-tab-list="caseCountTabList" not-has-padding hidden-border min-width="270px">
<template #item="{ item: tabItem }">
<div class="w-full">
<PassRatePie
:options="tabItem.options"
:tooltip-text="tabItem.tooltip"
:size="60"
:value-list="tabItem.valueList"
/>
</div>
</template>
</TabCard>
<div class="h-[148px]">
<MsChart :options="caseCountOptions" />
</div>
</div>
</div>
@ -38,53 +41,56 @@
* @desc 用例数量
*/
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue';
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
import TabCard from './tabCard.vue';
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
import { workCaseCountDetail } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { LegendData } from '@/models/apiTest/report';
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { StatusStatisticsMapType } from '@/models/workbench/homePage';
import { commonRatePieOptions, handlePieData } from '../utils';
const projectIds = ref('');
const appStore = useAppStore();
const { t } = useI18n();
const options = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['80%', '100%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [],
},
const props = defineProps<{
item: SelectedCardItem;
}>();
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const options = ref(cloneDeep(commonRatePieOptions));
function handlePassRatePercent(data: { name: string; count: number }[]) {
return data.slice(1).map((item) => {
return {
value: item.count,
label: item.name,
name: item.name,
};
});
}
const reviewValueList = ref([
{
label: t('workbench.homePage.reviewed'),
@ -95,6 +101,7 @@
value: 2000,
},
]);
const passValueList = ref([
{
label: t('workbench.homePage.havePassed'),
@ -106,101 +113,104 @@
},
]);
const legendData = ref<LegendData[]>([
{
label: 'P0',
value: 'P0',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--danger-6))] ml-[24px]',
},
{
label: 'P1',
value: 'P1',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--warning-6))] ml-[24px]',
},
{
label: 'P2',
value: 'P2',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
},
{
label: 'P3',
value: 'P3',
rote: 30,
count: 3,
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
},
]);
//
const executeCharOptions = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
{
value: 0,
name: t('common.fakeError'),
itemStyle: {
color: '#FFC14E',
},
},
{
value: 0,
name: t('common.fail'),
itemStyle: {
color: '#ED0303',
},
},
{
value: 0,
name: t('common.unExecute'),
itemStyle: {
color: '#D4D4D8',
},
},
{
value: 0,
name: t('common.block'),
itemStyle: {
color: '#B379C8',
},
},
],
},
const reviewOptions = ref<Record<string, any>>(cloneDeep(options.value));
const passOptions = ref<Record<string, any>>(cloneDeep(options.value));
const caseCountTabList = computed(() => {
return [
{
label: '',
value: 'execution',
valueList: reviewValueList.value,
options: { ...reviewOptions.value },
tooltip: 'workbench.homePage.reviewRateTooltip',
},
{
label: '',
value: 'pass',
valueList: passValueList.value,
options: { ...passOptions.value },
tooltip: 'workbench.homePage.reviewPassRateTooltip',
},
];
});
</script>
<style scoped lang="less">
.card-wrapper {
margin: 16px 0;
padding: 24px;
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply rounded-xl bg-white;
.title {
font-size: 16px;
@apply font-medium;
}
.case-count-wrapper {
@apply flex items-center gap-4;
.case-count-item {
@apply flex-1;
}
// X
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
const { review, pass } = statusStatisticsMap;
reviewValueList.value = handlePassRatePercent(review);
passValueList.value = handlePassRatePercent(pass);
reviewOptions.value.series.data = handlePassRatePercent(review);
passOptions.value.series.data = handlePassRatePercent(pass);
reviewOptions.value.title.text = review[0].name ?? '';
reviewOptions.value.title.subtext = `${review[0].count ?? 0}%`;
passOptions.value.title.text = pass[0].name ?? '';
passOptions.value.title.subtext = `${pass[0].count ?? 0}%`;
reviewOptions.value.series.color = ['#00C261', '#D4D4D8'];
passOptions.value.series.color = ['#00C261', '#ED0303'];
}
const caseCountOptions = ref<Record<string, any>>({});
async function initCaseCount() {
try {
const { startTime, endTime, dayNumber } = timeForm.value;
const params = {
current: 1,
pageSize: 5,
startTime: dayNumber ? null : startTime,
endTime: dayNumber ? null : endTime,
dayNumber: dayNumber ?? null,
projectIds: innerProjectIds.value,
organizationId: appStore.currentOrgId,
handleUsers: [],
};
const detail = await workCaseCountDetail(params);
const { statusStatisticsMap, statusPercentList } = detail;
caseCountOptions.value = handlePieData(props.item.key, statusPercentList);
handleRatePieData(statusStatisticsMap);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
</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>
<div class="card-wrapper">
<div class="card-wrapper card-min-height">
<div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.numberOfCaseReviews') }} </div>
<div>
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
value-key="id"
label-key="name"
@ -17,14 +16,19 @@
</MsSelect>
</div>
</div>
<div class="my-[16px]">
<div class="case-count-wrapper">
<div class="mt-[16px]">
<div class="case-count-wrapper mb-[16px]">
<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 class="mt-[16px]">
<SetReportChart size="120px" :legend-data="legendData" :options="executeCharOptions" :request-total="100000" />
<div class="h-[148px]">
<MsChart :options="caseReviewCountOptions" />
</div>
</div>
</div>
@ -35,53 +39,43 @@
* @desc 用例评审数量
*/
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue';
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { LegendData } from '@/models/apiTest/report';
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { commonRatePieOptions, handlePieData } from '../utils';
const { t } = useI18n();
const appStore = useAppStore();
const projectIds = ref('');
const { t } = useI18n();
const props = defineProps<{
item: SelectedCardItem;
}>();
const options = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['80%', '100%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [],
},
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const options = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
const coverValueList = ref([
{
label: t('workbench.homePage.covered'),
@ -93,83 +87,79 @@
},
]);
const legendData = ref<LegendData[]>([
{
label: t('common.notStarted'),
value: 'P0',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--primary-4))] ml-[24px]',
},
{
label: t('common.inProgress'),
value: 'P1',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
},
{
label: t('common.completed'),
value: 'P2',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
},
{
label: t('common.archived'),
value: 'P3',
rote: 30,
count: 3,
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
},
]);
const executeCharOptions = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
{
value: 0,
name: t('common.fakeError'),
itemStyle: {
color: '#FFC14E',
},
},
{
value: 0,
name: t('common.fail'),
itemStyle: {
color: '#ED0303',
},
},
{
value: 0,
name: t('common.unExecute'),
itemStyle: {
color: '#D4D4D8',
},
},
{
value: 0,
name: t('common.block'),
itemStyle: {
color: '#B379C8',
},
},
// TODO
const detail = ref<PassRateDataType>({
statusStatisticsMap: {
cover: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
},
statusPercentList: [
{ status: '未开始', count: 1, percentValue: '10%' },
{ status: '进行中', count: 3, percentValue: '0%' },
{ status: '已完成', count: 6, percentValue: '0%' },
{ status: '已归档', count: 7, percentValue: '0%' },
],
});
const caseReviewCountOptions = ref<Record<string, any>>({});
function initApiCount() {
const { statusStatisticsMap, statusPercentList } = detail.value;
caseReviewCountOptions.value = handlePieData(props.item.key, statusPercentList);
const { cover } = statusStatisticsMap;
coverValueList.value = cover.slice(1).map((item) => {
return {
value: item.count,
label: item.name,
name: item.name,
};
});
options.value.series.data = coverValueList.value;
options.value.title.text = cover[0].name ?? '';
options.value.title.subtext = `${cover[0].count ?? 0}%`;
options.value.series.color = ['#00C261', '#D4D4D8'];
}
onMounted(() => {
initApiCount();
});
watch(
() => innerProjectIds.value,
(val) => {
if (val) {
const [newProjectId] = val;
projectId.value = newProjectId;
initApiCount();
}
}
);
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
initApiCount();
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
initApiCount();
}
},
{
deep: true,
}
);
</script>
<style scoped lang="less"></style>

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>
<CountOverview :value-list="valueList" :legend-data="legendData" :title="title" />
<div class="card-wrapper card-min-height">
<div class="flex items-center justify-between">
<div class="title"> {{ t(props.item.label) }} </div>
<div>
<MsSelect
v-model:model-value="projectId"
:options="appStore.projectList"
allow-search
value-key="id"
label-key="name"
:search-keys="['name']"
class="!w-[240px]"
:prefix="t('workbench.homePage.project')"
>
</MsSelect>
</div>
</div>
<div class="mt-[16px]">
<div class="case-count-wrapper">
<div class="case-count-item mb-[16px]">
<PassRatePie :tooltip-text="tooltip" :options="legacyOptions" :size="60" :value-list="valueList" />
</div>
</div>
<div class="h-[148px]">
<MsChart :options="countOptions" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
/** *
* @desc 待我处理的缺陷&缺陷数量
* @desc 用于缺陷数量待我处理的缺陷数量组件
*/
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import CountOverview from './countOverview.vue';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { LegendData } from '@/models/apiTest/report';
import type {
PassRateDataType,
SelectedCardItem,
StatusStatisticsMapType,
TimeFormParams,
} from '@/models/workbench/homePage';
import { WorkCardEnum } from '@/enums/workbenchEnum';
import { commonRatePieOptions, handlePieData } from '../utils';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
type: WorkCardEnum;
item: SelectedCardItem;
}>();
const title = computed(() => {
switch (props.type) {
case WorkCardEnum.HANDLE_BUG_BY_ME:
return t('workbench.homePage.pendingDefect');
case WorkCardEnum.CREATE_BUG_BY_ME:
return t('workbench.homePage.createdBugByMe');
case WorkCardEnum.PLAN_LEGACY_BUG:
return t('workbench.homePage.remainingBugOfPlan');
default:
return t('workbench.homePage.bugCount');
}
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = ref<string>(innerProjectIds.value[0]);
const valueList = ref<
{
label: string;
@ -49,36 +83,113 @@
},
]);
const legendData = ref<LegendData[]>([
{
label: t('common.notStarted'),
value: 'notStarted',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--primary-4))] ml-[24px]',
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const legacyOptions = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
// TODO
const detail = ref<PassRateDataType>({
statusStatisticsMap: {
legacy: [
{ name: '遗留率', count: 10 },
{ name: '缺陷总数', count: 2 },
{ name: '遗留缺陷数', count: 1 },
],
},
statusPercentList: [
{ status: 'AAA', count: 1, percentValue: '10%' },
{ status: 'BBB', count: 3, percentValue: '0%' },
{ status: 'CCC', count: 6, percentValue: '0%' },
],
});
const countOptions = ref({});
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
const { legacy } = statusStatisticsMap;
valueList.value = legacy.slice(1).map((item) => {
return {
value: item.count,
label: item.name,
name: item.name,
};
});
legacyOptions.value.series.data = valueList.value;
legacyOptions.value.title.text = legacy[0].name ?? '';
legacyOptions.value.title.subtext = `${legacy[0].count ?? 0}%`;
legacyOptions.value.series.color = ['#D4D4D8', '#00C261'];
}
async function initCount() {
try {
const { startTime, endTime, dayNumber } = timeForm.value;
const params = {
current: 1,
pageSize: 5,
startTime: dayNumber ? null : startTime,
endTime: dayNumber ? null : endTime,
dayNumber: dayNumber ?? null,
projectIds: innerProjectIds.value,
organizationId: appStore.currentOrgId,
handleUsers: [],
};
const { statusStatisticsMap, statusPercentList } = detail.value;
countOptions.value = handlePieData(props.item.key, statusPercentList);
handleRatePieData(statusStatisticsMap);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const tooltip = computed(() => {
return props.item.key === WorkCardEnum.PLAN_LEGACY_BUG ? 'workbench.homePage.planCaseCountLegacyRateTooltip' : '';
});
onMounted(() => {
initCount();
});
watch(
() => innerProjectIds.value,
(val) => {
if (val) {
const [newProjectId] = val;
projectId.value = newProjectId;
initCount();
}
}
);
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
initCount();
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
initCount();
}
},
{
label: t('common.inProgress'),
value: 'inProgress',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
},
{
label: t('common.completed'),
value: 'completed',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
},
{
label: t('common.archived'),
value: 'archived',
rote: 30,
count: 3,
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
},
]);
deep: true,
}
);
</script>
<style scoped lang="less"></style>

View File

@ -1,10 +1,10 @@
<template>
<div class="card-wrapper">
<div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.defectProcessingNumber') }} </div>
<div class="title"> {{ t(props.item.label) }} </div>
<div class="flex items-center gap-[8px]">
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
@ -30,7 +30,7 @@
</div>
</div>
<div class="mt-[16px]">
<MsChart height="300px" :options="options" />
<MsChart height="260px" :options="options" />
</div>
</div>
</template>
@ -47,14 +47,38 @@
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { defectStatusColor, getCommonBarOptions } from '../utils';
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { commonColorConfig, getCommonBarOptions } from '../utils';
import type { SelectOptionData } from '@arco-design/web-vue';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
item: SelectedCardItem;
}>();
const memberIds = ref('');
const projectIds = ref('');
const appStore = useAppStore();
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = computed<string>({
get: () => {
const [newProject] = innerProjectIds.value;
return newProject;
},
set: (val) => val,
});
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const memberOptions = ref<SelectOptionData[]>([]);
@ -63,30 +87,139 @@
const hasRoom = computed(() => members.value.length >= 7);
const seriesData = ref<Record<string, any>[]>([
{
name: '已结束',
name: '新创建',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
borderRadius: [2, 2, 0, 0],
// borderRadius: [2, 2, 0, 0],
},
data: [400, 200, 400, 200, 400, 200],
},
{
name: '未结束',
name: '激活',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '处理中',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '已关闭',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '新创建1',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [400, 200, 400, 200, 400, 200],
},
{
name: '激活1',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '处理中1',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '已关闭1',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '已关闭2',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
// borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
{
name: '已关闭3',
type: 'bar',
barWidth: 12,
stack: 'bug',
itemStyle: {
borderRadius: [2, 2, 0, 0],
},
data: [90, 160, 90, 160, 90, 160],
},
]);
const defectStatusColor = ['#811FA3', '#FFA200', '#3370FF', '#F24F4F'];
onMounted(() => {
options.value = getCommonBarOptions(hasRoom.value, defectStatusColor);
function getDefectMemberDetail() {
options.value = getCommonBarOptions(hasRoom.value, [...defectStatusColor, ...commonColorConfig]);
options.value.xAxis.data = members.value;
options.value.series = seriesData.value;
}
onMounted(() => {
getDefectMemberDetail();
});
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
getDefectMemberDetail();
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
getDefectMemberDetail();
}
},
{
deep: true,
}
);
</script>
<style scoped></style>

View File

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

View File

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

View File

@ -1,6 +1,16 @@
<template>
<div class="pass-rate-content">
<MsChart :height="`${props.size}px`" :width="`${props.size}px`" :options="options" />
<div class="relative flex items-center justify-center">
<a-tooltip
v-if="props.tooltipText"
:mouse-enter-delay="500"
:content="t(props.tooltipText || '')"
position="bottom"
>
<div class="tooltip-rate h-[50px] w-[50px]"></div>
</a-tooltip>
<MsChart :height="`${props.size}px`" :width="`${props.size}px`" :options="props.options" />
</div>
<div class="pass-rate-title flex-1">
<div v-for="item of props.valueList" :key="item.label" class="flex-1">
<div class="mb-[8px] text-[var(--color-text-4)]">{{ item.label }}</div>
@ -23,6 +33,7 @@
const props = defineProps<{
options: Record<string, any>;
size: number;
tooltipText?: string;
valueList: {
label: string;
value: number;
@ -46,4 +57,9 @@
}
}
}
.tooltip-rate {
position: absolute;
z-index: 9;
border-radius: 50%;
}
</style>

View File

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

View File

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

View File

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

View File

@ -1,14 +1,13 @@
<template>
<div class="card-wrapper">
<div class="card-wrapper card-min-height">
<div class="flex items-center justify-between">
<div class="title">
{{ t('workbench.homePage.numberOfTestPlan') }}
</div>
<div>
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
value-key="id"
label-key="name"
@ -20,15 +19,15 @@
</div>
</div>
<div class="mt-[16px]">
<TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border>
<template #item="{ item }">
<TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border min-width="270px">
<template #item="{ item: tabItem }">
<div class="w-full">
<PassRatePie :options="options" :size="60" :value-list="item.valueList" />
<PassRatePie :options="tabItem.options" :size="60" :value-list="tabItem.valueList" />
</div>
</template>
</TabCard>
<div class="mt-[16px]">
<SetReportChart size="120px" :legend-data="legendData" :options="testPlanCharOptions" :request-total="100000" />
<div class="h-[148px]">
<MsChart :options="testPlanCountOptions" />
</div>
</div>
</div>
@ -39,54 +38,78 @@
* @desc 测试计划数量
*/
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.vue';
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
import { commonConfig, seriesConfig, toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { LegendData } from '@/models/apiTest/report';
import { WorkCardEnum } from '@/enums/workbenchEnum';
import type {
PassRateDataType,
SelectedCardItem,
StatusStatisticsMapType,
TimeFormParams,
} from '@/models/workbench/homePage';
import { commonRatePieOptions, handlePieData } from '../utils';
const props = defineProps<{
item: SelectedCardItem;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const projectIds = ref('');
const options = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['80%', '100%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [],
},
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
// TODO
const detail = ref<PassRateDataType>({
statusStatisticsMap: {
execute: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
pass: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
complete: [
{ name: '覆盖率', count: 10 },
{ name: '已覆盖', count: 2 },
{ name: '未覆盖', count: 1 },
],
},
statusPercentList: [
{ status: 'HTTP', count: 1, percentValue: '10%' },
{ status: 'TCP', count: 3, percentValue: '0%' },
{ status: 'BBB', count: 6, percentValue: '0%' },
],
});
const options = ref(cloneDeep(commonRatePieOptions));
const executionOptions = ref<Record<string, any>>(cloneDeep(options.value));
const passOptions = ref<Record<string, any>>(cloneDeep(options.value));
const completeOptions = ref<Record<string, any>>(cloneDeep(options.value));
//
const executionValueList = ref([
{
@ -125,106 +148,127 @@
value: 2000,
},
]);
const testPlanTabList = computed(() => {
return [
{
label: '',
value: 'execution',
valueList: executionValueList.value,
options,
options: { ...executionOptions.value },
},
{
label: '',
value: 'pass',
valueList: passValueList.value,
options,
options: { ...passOptions.value },
},
{
label: '',
value: 'complete',
valueList: completeValueList.value,
options,
options: { ...completeOptions.value },
},
];
});
const testPlanCharOptions = ref({
...commonConfig,
tooltip: {
...toolTipConfig,
},
series: {
...seriesConfig,
data: [
{
value: 0,
name: t('common.success'),
itemStyle: {
color: '#00C261',
},
},
{
value: 0,
name: t('common.fakeError'),
itemStyle: {
color: '#FFC14E',
},
},
{
value: 0,
name: t('common.fail'),
itemStyle: {
color: '#ED0303',
},
},
{
value: 0,
name: t('common.unExecute'),
itemStyle: {
color: '#D4D4D8',
},
},
{
value: 0,
name: t('common.block'),
itemStyle: {
color: '#B379C8',
},
},
],
},
function handlePassRatePercent(data: { name: string; count: number }[]) {
return data.slice(1).map((item) => {
return {
value: item.count,
label: item.name,
name: item.name,
};
});
}
function handleRatePieData(statusStatisticsMap: StatusStatisticsMapType) {
const { execute, pass, complete } = statusStatisticsMap;
executionValueList.value = handlePassRatePercent(execute);
passValueList.value = handlePassRatePercent(pass);
completeValueList.value = handlePassRatePercent(complete);
executionOptions.value.series.data = handlePassRatePercent(execute);
passOptions.value.series.data = handlePassRatePercent(pass);
completeOptions.value.series.data = handlePassRatePercent(complete);
executionOptions.value.title.text = execute[0].name ?? '';
executionOptions.value.title.subtext = `${execute[0].count ?? 0}%`;
passOptions.value.title.text = pass[0].name ?? '';
passOptions.value.title.subtext = `${pass[0].count ?? 0}%`;
completeOptions.value.title.text = complete[0].name ?? '';
completeOptions.value.title.subtext = `${complete[0].count ?? 0}%`;
executionOptions.value.series.color = ['#D4D4D8', '#00C261'];
passOptions.value.series.color = ['#D4D4D8', '#00C261'];
completeOptions.value.series.color = ['#00C261', '#3370FF', '#D4D4D8'];
}
const testPlanCountOptions = ref({});
async function initTestPlanCount() {
try {
const { startTime, endTime, dayNumber } = timeForm.value;
const params = {
current: 1,
pageSize: 5,
startTime: dayNumber ? null : startTime,
endTime: dayNumber ? null : endTime,
dayNumber: dayNumber ?? null,
projectIds: innerProjectIds.value,
organizationId: appStore.currentOrgId,
handleUsers: [],
};
const { statusStatisticsMap, statusPercentList } = detail.value;
testPlanCountOptions.value = handlePieData(props.item.key, statusPercentList);
handleRatePieData(statusStatisticsMap);
} catch (error) {
console.log(error);
}
}
onMounted(() => {
initTestPlanCount();
});
const legendData = ref<LegendData[]>([
{
label: t('common.notStarted'),
value: 'notStarted',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--primary-4))] ml-[24px]',
onMounted(() => {
initTestPlanCount();
});
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
initTestPlanCount();
}
}
);
watch(
() => innerProjectIds.value,
(val) => {
if (val) {
const [newProjectId] = val;
projectId.value = newProjectId;
initTestPlanCount();
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
initTestPlanCount();
}
},
{
label: t('common.inProgress'),
value: 'inProgress',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--link-6))] ml-[24px]',
},
{
label: t('common.completed'),
value: 'completed',
rote: 30,
count: 3,
class: 'bg-[rgb(var(--success-6))] ml-[24px]',
},
{
label: t('common.archived'),
value: 'archived',
rote: 30,
count: 3,
class: 'bg-[var(--color-text-input-border)] ml-[24px]',
},
]);
deep: true,
}
);
</script>
<style scoped lang="less">

View File

@ -1,12 +1,11 @@
<template>
<div class="card-wrapper">
<div class="flex items-center justify-between">
<div class="title"> {{ t('workbench.homePage.waitForReview') }} </div>
<div class="title"> {{ t(props.item.label) }} </div>
<div>
<MsSelect
v-model:model-value="projectIds"
v-model:model-value="projectId"
:options="appStore.projectList"
allow-clear
allow-search
value-key="id"
label-key="name"
@ -78,13 +77,30 @@
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { SelectOptionData } from '@arco-design/web-vue';
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
const appStore = useAppStore();
const { t } = useI18n();
const projectIds = ref('');
const projectOptions = ref<SelectOptionData[]>([]);
const props = defineProps<{
item: SelectedCardItem;
}>();
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const projectId = ref<string>(innerProjectIds.value[0]);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
ref({
dayNumber: 3,
startTime: 0,
endTime: 0,
})
);
const columns: MsTableColumn = [
{
title: 'ID',
@ -122,10 +138,55 @@
showSelectAll: false,
});
onMounted(() => {
setLoadListParams({});
function initData() {
const { startTime, endTime, dayNumber } = timeForm.value;
setLoadListParams({
current: 1,
pageSize: 5,
startTime: dayNumber ? null : startTime,
endTime: dayNumber ? null : endTime,
dayNumber: dayNumber ?? null,
projectIds: innerProjectIds.value,
organizationId: appStore.currentOrgId,
handleUsers: [],
});
loadList();
}
onMounted(() => {
initData();
});
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
initData();
}
}
);
watch(
() => projectId.value,
(val) => {
if (val) {
innerProjectIds.value = [val];
}
}
);
watch(
() => timeForm.value,
(val) => {
if (val) {
initData();
}
},
{
deep: true,
}
);
</script>
<style scoped></style>

View File

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

View File

@ -79,4 +79,31 @@ export default {
'workbench.homePage.fullScreen': 'Full screen',
'workbench.homePage.sort': 'Sort',
'workbench.homePage.workNoProjectTip': 'Workbench no content, please join the project',
'workbench.homePage.notHasData': 'No data',
'workbench.homePage.notHasResPermission': 'No resource permission',
'workbench.homePage.reviewRateTooltip':
'Review Rate: Reviewed functional test cases / All functional test cases * 100%',
'workbench.homePage.reviewPassRateTooltip':
'Review Pass Rate: Passed reviewed test cases / All reviewed test cases * 100%',
'workbench.homePage.associateCaseCoverRateTooltip':
'Association Coverage Rate: Associated functional test cases / All functional test cases * 100%',
'workbench.homePage.caseReviewCoverRateTooltip':
'Reviewed Test Case Coverage Rate: Reviewed test cases / All test cases * 100%',
'workbench.homePage.apiCountCoverRateTooltip':
'API Coverage Rate: APIs (URLs) with (test cases or scenario steps) / Total APIs * 100%',
'workbench.homePage.apiCountCompleteRateTooltip': 'API Completion Rate: Completed APIs / Total APIs * 100%',
'workbench.homePage.apiCaseCountCoverRateTooltip':
'API Test Case Coverage Rate: APIs with test cases / Total APIs * 100%',
'workbench.homePage.apiCaseCountExecuteRateTooltip':
'Test Case Execution Rate: Executed API test cases / All API test cases * 100%',
'workbench.homePage.apiCaseCountPassRateTooltip':
'Test Case Pass Rate: Last successful execution test cases / Total test cases * 100%',
'workbench.homePage.scenarioCaseCountCoverRateTooltip':
'Scenario Coverage Rate: APIs (URLs) included in scenario steps / Total APIs * 100%',
'workbench.homePage.scenarioCaseCountExecuteRateTooltip':
'Scenario Execution Rate: Executed scenarios / All scenarios * 100%',
'workbench.homePage.scenarioCaseCountPassRateTooltip':
'Scenario Pass Rate: Last successful execution scenarios / Total scenarios * 100%',
'workbench.homePage.planCaseCountLegacyRateTooltip':
'Legacy Rate: Unresolved defects / All associated defects * 100%',
};

View File

@ -9,7 +9,7 @@ export default {
'workbench.homePage.interfaceCASE': '接口 CASE',
'workbench.homePage.interfaceScenario': '接口场景',
'workbench.homePage.apiPlan': '接口计划',
'workbench.homePage.bugCount': '缺陷数',
'workbench.homePage.bugCount': '缺陷数',
'workbench.homePage.nearlyThreeDays': '近3天',
'workbench.homePage.nearlySevenDays': '近7天',
'workbench.homePage.customize': '自定义',
@ -18,8 +18,8 @@ export default {
'workbench.homePage.staff': '人员',
'workbench.homePage.workEmptyConfig': '工作台暂无内容,立即',
'workbench.homePage.configureWorkbench': '配置工作台',
'workbench.homePage.useCasesCount': '关联用例数',
'workbench.homePage.useCasesNumber': '用例数',
'workbench.homePage.useCasesCount': '关联用例数',
'workbench.homePage.useCasesNumber': '用例数',
'workbench.homePage.reviewed': '已评审',
'workbench.homePage.unReviewed': '未评审',
'workbench.homePage.havePassed': '已通过',
@ -30,27 +30,27 @@ export default {
'workbench.homePage.sceneUseCase': '场景用例',
'workbench.homePage.waitForReview': '待我评审',
'workbench.homePage.executionTimes': '执行次数',
'workbench.homePage.apiUseCasesNumber': '接口用例数',
'workbench.homePage.misstatementCount': '误报数',
'workbench.homePage.apiUseCasesNumber': '接口用例数',
'workbench.homePage.misstatementCount': '误报数',
'workbench.homePage.apiCoverage': '接口覆盖率',
'workbench.homePage.caseExecutionRate': '用例执行率',
'workbench.homePage.casePassedRate': '用例通过率',
'workbench.homePage.sceneExecutionRate': '场景执行率',
'workbench.homePage.executionRate': '执行通过率',
'workbench.homePage.scenarioUseCasesNumber': '场景用例数',
'workbench.homePage.scenarioUseCasesNumber': '场景用例数',
'workbench.homePage.interfaceChange': '接口变更',
'workbench.homePage.associationCASE': '关联CASE',
'workbench.homePage.associatedScene': '关联场景',
'workbench.homePage.pendingDefect': '待我处理的缺陷',
'workbench.homePage.defectProcessingNumber': '缺陷处理人数',
'workbench.homePage.defectProcessingNumber': '缺陷处理人数',
'workbench.homePage.defectTotal': '缺陷总数',
'workbench.homePage.legacyDefectsNumber': '遗留缺陷数',
'workbench.homePage.createdBugByMe': '我创建的缺陷',
'workbench.homePage.remainingBugOfPlan': '计划遗留缺陷数',
'workbench.homePage.apiCount': '接口数',
'workbench.homePage.remainingBugOfPlan': '计划遗留缺陷数',
'workbench.homePage.apiCount': '接口数',
'workbench.homePage.unFinish': '未完成',
'workbench.homePage.numberOfTestPlan': '测试计划数',
'workbench.homePage.numberOfCaseReviews': '用例评审数',
'workbench.homePage.numberOfTestPlan': '测试计划数',
'workbench.homePage.numberOfCaseReviews': '用例评审数',
'workbench.homePage.projectOverview': '项目概览',
'workbench.homePage.projectOverviewDesc': '统计所在项目的资源及分布的数据统计',
'workbench.homePage.staffOverviewDesc': '统计成员在所选项目中创建的资源及分布的数据统计',
@ -77,4 +77,19 @@ export default {
'workbench.homePage.fullScreen': '全屏',
'workbench.homePage.sort': '排序',
'workbench.homePage.workNoProjectTip': '工作台暂无内容,请先加入项目',
'workbench.homePage.notHasData': '无数据',
'workbench.homePage.notHasResPermission': '无资源权限',
'workbench.homePage.reviewRateTooltip': '评审率: 已评审功能用例/所有功能用例 * 100%',
'workbench.homePage.reviewPassRateTooltip': '评审通过率:已评审通过的用例/所有完成评审的用例*100%',
'workbench.homePage.associateCaseCoverRateTooltip': '覆盖率:关联的功能用例/所有功能用例 * 100%',
'workbench.homePage.caseReviewCoverRateTooltip': '覆盖率:已评审的用例/所有的用例*100%',
'workbench.homePage.apiCountCoverRateTooltip': '接口覆盖率接口URL用例或场景步骤数/接口总数*100%',
'workbench.homePage.apiCountCompleteRateTooltip': '接口完成率:已完成的接口/接口总数*100%',
'workbench.homePage.apiCaseCountCoverRateTooltip': '接口覆盖率:有用例的接口/接口总数*100%',
'workbench.homePage.apiCaseCountExecuteRateTooltip': '用例执行率:执行过的接口用例/所有接口用例 * 100%',
'workbench.homePage.apiCaseCountPassRateTooltip': '用例通过率:最后一次执行成功的用例/用例总数*100%',
'workbench.homePage.scenarioCaseCountCoverRateTooltip': '接口覆盖率:被场景步骤包含的接口(URL)数/接口总数*100%',
'workbench.homePage.scenarioCaseCountExecuteRateTooltip': '场景执行率:执行过的场景/所有场景 * 100%',
'workbench.homePage.scenarioCaseCountPassRateTooltip': '场景通过率:最后一次执行成功的场景/场景总数*100%',
'workbench.homePage.planCaseCountLegacyRateTooltip': '遗留率:未关闭缺陷/所有关联的缺陷*100%',
};

View File

@ -1,5 +1,12 @@
import { commonConfig, toolTipConfig } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type { ModuleCardItem } from '@/models/workbench/homePage';
import { WorkCardEnum, WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
const { t } = useI18n();
// 通用颜色配置
export const commonColorConfig = [
'#811FA3',
'#00C261',
@ -26,28 +33,71 @@ export const commonColorConfig = [
'#87F578',
];
export const defectStatusColor = ['#00C261', '#FFA200'];
// 饼图颜色配置
export const colorMapConfig: Record<string, string[]> = {
[WorkCardEnum.CASE_COUNT]: ['#ED0303', '#FFA200', '#3370FF', '#D4D4D8'],
[WorkCardEnum.ASSOCIATE_CASE_COUNT]: ['#00C261', '#3370FF'],
[WorkCardEnum.REVIEW_CASE_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.TEST_PLAN_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.PLAN_LEGACY_BUG]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.BUG_COUNT]: ['#FFA200', '#00C261', '#D4D4D8'],
[WorkCardEnum.HANDLE_BUG_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.CREATE_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
[WorkCardEnum.API_COUNT]: ['#811FA3', '#00C261', '#3370FF', '#FFA1FF', '#EE50A3', '#FF9964', '#F9F871', '#C3DD40'],
[WorkCardEnum.CREATE_BUG_BY_ME]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
};
// 柱状图
export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<string, any> {
return {
tooltip: {
trigger: 'item',
borderWidth: 0,
formatter(params: any) {
const html = `
<div class="w-[186px] h-[50px] p-[16px] flex items-center justify-between">
<div class=" flex items-center">
<div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm bg-[${params.color}]" style="background:${
params.color
}"></div>
<div style="color:#959598">${params.name}</div>
</div>
<div class="text-[#323233] font-medium">${addCommasToNumber(params.value)}</div>
</div>
tooltip: [
{
trigger: 'axis',
borderWidth: 0,
padding: 0,
label: {
width: 50,
overflow: 'truncate',
},
displayMode: 'single',
enterable: true,
// TODO 单例模式
// formatter(params: any) {
// const html = `
// <div class="w-[186px] h-[50px] p-[16px] flex items-center justify-between">
// <div class=" flex items-center">
// <div class="mb-[2px] mr-[8px] h-[8px] w-[8px] rounded-sm bg-[${params.color}]" style="background:${
// params.color
// }"></div>
// <div style="color:#959598">${params.name}</div>
// </div>
// <div class="text-[#323233] font-medium">${addCommasToNumber(params.value)}</div>
// </div>
// `;
// return html;
// },
formatter(params: any) {
const html = `
<div class="w-[186px] 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,
grid: {
top: '36px',
@ -63,6 +113,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
data: [],
axisLabel: {
color: '#646466',
interval: 0,
},
axisTick: {
show: false, // 隐藏刻度线
@ -81,7 +132,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
nameTextStyle: {
fontSize: 12,
color: '#AEAEB2', // 自定义字体大小和颜色
padding: [0, 0, 0, -20], // 通过左侧(最后一个值)的负偏移向左移动
padding: [0, 0, 0, 10], // 通过padding控制Y轴单位距离左侧的距离
},
nameGap: 20,
splitLine: {
@ -94,14 +145,41 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
},
},
],
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '',
fontSize: 14,
fill: '#959598',
backgroundColor: '#F9F9FE',
padding: [6, 16, 6, 16],
borderRadius: 4,
},
invisible: true,
},
colorBy: 'series',
series: [],
barCategoryGap: '50%', // 控制 X 轴分布居中效果
legend: {
width: '60%',
show: true,
type: 'scroll',
itemGap: 20,
itemWidth: 8,
itemHeight: 8,
left: 'center',
pageButtonItemGap: 5,
pageButtonGap: 5,
pageIconColor: '#00000099',
pageIconInactiveColor: '#00000042',
pageIconSize: [10, 8],
pageTextStyle: {
color: '#00000099',
fontSize: 12,
},
},
dataZoom: hasRoom
? [
@ -116,4 +194,353 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
};
}
export default {};
export const contentTabList = ref<ModuleCardItem[]>([
{
label: t('workbench.homePage.functionalUseCase'),
value: WorkOverviewEnum.FUNCTIONAL,
icon: WorkOverviewIconEnum.FUNCTIONAL,
color: 'rgb(var(--primary-5))',
count: 0,
},
{
label: t('workbench.homePage.useCaseReview'),
value: WorkOverviewEnum.CASE_REVIEW,
icon: WorkOverviewIconEnum.CASE_REVIEW,
color: 'rgb(var(--success-6))',
count: 0,
},
{
label: t('workbench.homePage.interfaceAPI'),
value: WorkOverviewEnum.API,
icon: WorkOverviewIconEnum.API,
color: 'rgb(var(--link-6))',
count: 0,
},
{
label: t('workbench.homePage.interfaceCASE'),
value: WorkOverviewEnum.API_CASE,
icon: WorkOverviewIconEnum.API_CASE,
color: 'rgb(var(--link-6))',
count: 0,
},
{
label: t('workbench.homePage.interfaceScenario'),
value: WorkOverviewEnum.API_SCENARIO,
icon: WorkOverviewIconEnum.API_SCENARIO,
color: 'rgb(var(--link-6))',
count: 0,
},
{
label: t('workbench.homePage.apiPlan'),
value: WorkOverviewEnum.TEST_PLAN,
icon: WorkOverviewIconEnum.TEST_PLAN,
color: 'rgb(var(--link-6))',
count: 0,
},
{
label: t('workbench.homePage.bugCount'),
value: WorkOverviewEnum.BUG_COUNT,
icon: WorkOverviewIconEnum.BUG_COUNT,
color: 'rgb(var(--danger-6))',
count: 0,
},
]);
// 下方饼图配置
export function getPieCharOptions(key: WorkCardEnum) {
return {
title: {
show: true,
text: '总数(个)',
left: 85,
top: '30%',
textStyle: {
fontSize: 12,
fontWeight: 'normal',
color: '#959598',
},
subtext: '100111',
subtextStyle: {
fontSize: 20,
color: '#323233',
fontWeight: 'bold',
align: 'center',
},
textAlign: 'center', // 确保副标题居中
},
color: colorMapConfig[key],
tooltip: {
...toolTipConfig,
position: 'right',
},
legend: {
width: '100%',
height: 128,
type: 'scroll',
orient: 'vertical',
pageButtonItemGap: 5,
pageButtonGap: 5,
pageIconColor: '#00000099',
pageIconInactiveColor: '#00000042',
pageIconSize: [7, 5],
pageTextStyle: {
color: '#00000099',
fontSize: 12,
},
pageButtonPosition: 'end',
itemGap: 16,
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
bottom: 'center',
left: 180,
textStyle: {
color: '#333',
fontSize: 14, // 字体大小
textBorderType: 'solid',
rich: {
a: {
width: 50,
color: '#959598',
fontSize: 12,
align: 'left',
},
b: {
width: 50,
color: '#323233',
fontSize: 12,
fontWeight: 'bold',
align: 'right',
},
c: {
width: 50,
color: '#323233',
fontSize: 12,
fontWeight: 'bold',
align: 'right',
},
},
},
},
media: [
{
query: { maxWidth: 600 },
option: {
legend: {
textStyle: {
width: 200,
},
},
},
},
{
query: { minWidth: 601, maxWidth: 800 },
option: {
legend: {
textStyle: {
width: 450,
},
},
},
},
{
query: { minWidth: 801, maxWidth: 1200 },
option: {
legend: {
textStyle: {
width: 600,
},
},
},
},
{
query: { minWidth: 1201 },
option: {
legend: {
textStyle: {
width: 1000,
},
},
},
},
],
series: {
name: '',
type: 'pie',
padAngle: 2,
radius: ['75%', '90%'],
center: [90, '48%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [],
},
};
}
// 空数据和无权限处理
export function handleNoDataDisplay(
xAxis: string[],
projectCountList: { id: string; name: string; count: number[] }[]
) {
if (!xAxis.length) {
return {
invisible: false,
text: t('workbench.homePage.notHasResPermission'),
};
}
const isEmptyData = projectCountList.every((item) =>
item.count.every((e) => e === 0 || e === null || e === undefined)
);
if (isEmptyData) {
return {
invisible: false,
text: t('workbench.homePage.notHasData'),
};
}
return {
invisible: true,
text: '',
};
}
// XX率饼图配置
export const commonRatePieOptions = {
...commonConfig,
title: {
show: true,
text: '',
left: 26,
top: '20%',
textStyle: {
fontSize: 12,
fontWeight: 'normal',
color: '#959598',
},
triggerEvent: true, // 开启鼠标事件
subtext: '0',
subtextStyle: {
fontSize: 12,
color: '#323233',
fontWeight: 'bold',
align: 'center',
lineHeight: 3,
},
textAlign: 'center',
tooltip: {
...toolTipConfig,
position: 'right',
},
},
tooltip: {
...toolTipConfig,
position: 'right',
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
color: [],
padAngle: 2,
radius: ['85%', '100%'],
center: [30, '50%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
scale: false, // 禁用放大效果
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [],
},
// graphic: [
// {
// type: 'text',
// left: 'center',
// top: '5%',
// style: {
// text: '饼图标题',
// fontSize: 18,
// fontWeight: 'bold',
// fill: '#333',
// cursor: 'pointer'
// },
// onmouseover (params) {
// // 悬浮到标题上时显示提示信息
// // chart.dispatchAction({
// // type: 'showTip',
// // position: [params.event.offsetX, params.event.offsetY],
// // // 配置提示内容
// // formatter: '这是饼图标题的提示内容'
// // });
// },
// onmouseout () {
// // 离开标题时隐藏提示信息
// // chart.dispatchAction({
// // type: 'hideTip'
// // });
// }
// }
// ]
};
// 统一处理下方饼图数据结构
export function handlePieData(
key: WorkCardEnum,
statusPercentList: {
status: string; // 状态
count: number;
percentValue: string; // 百分比
}[]
) {
const options: Record<string, any> = getPieCharOptions(key);
options.series.data = statusPercentList.map((item) => ({
name: item.status,
value: item.count,
}));
// 计算总数和图例格式
const tempObject: Record<string, any> = {};
let totalCount = 0;
statusPercentList.forEach((item) => {
tempObject[item.status] = item;
totalCount += item.count;
});
// 设置图例的格式化函数,显示百分比
options.legend.formatter = (name: string) => {
return `{a|${tempObject[name].status}} {b|${addCommasToNumber(tempObject[name].count)}} {c|${
tempObject[name].percentValue
}}`;
};
// 设置副标题为总数
options.title.subtext = addCommasToNumber(totalCount);
return options;
}

View File

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