feat(工作台): 工作台首页联调部分卡片
This commit is contained in:
parent
61c05433fa
commit
fced7b284f
|
@ -1,6 +1,6 @@
|
|||
import MSR from '@/api/http/index';
|
||||
|
||||
import type { ApiCaseDetail } from '@/models/apiTest/management';
|
||||
import type { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||
import type { ApiScenarioTableItem } from '@/models/apiTest/scenario';
|
||||
import type { BugListItem } from '@/models/bug-management';
|
||||
import type { ReviewItem } from '@/models/caseManagement/caseReview';
|
||||
|
@ -17,6 +17,7 @@ import type {
|
|||
import {
|
||||
EditDashboardLayoutUrl,
|
||||
GetDashboardLayoutUrl,
|
||||
WorkApiChangeListUrl,
|
||||
WorkAssociateCaseDetailUrl,
|
||||
WorkbenchApiCaseListUrl,
|
||||
WorkbenchBugListUrl,
|
||||
|
@ -25,10 +26,13 @@ import {
|
|||
WorkbenchScenarioListUrl,
|
||||
WorkbenchTestPlanListUrl,
|
||||
WorkbenchTestPlanStatisticUrl,
|
||||
WorkBugHandlerDetailUrl,
|
||||
WorkCaseCountDetailUrl,
|
||||
WorkCaseReviewDetailUrl,
|
||||
WorkMemberViewDetailUrl,
|
||||
WorkMyCreatedDetailUrl,
|
||||
WorkProOverviewDetailUrl,
|
||||
WorkReviewListUrl,
|
||||
WorkTodoBugListUrl,
|
||||
WorkTodoPlanListUrl,
|
||||
WorkTodoReviewListUrl,
|
||||
|
@ -100,6 +104,32 @@ export function workAssociateCaseDetail(data: WorkHomePageDetail) {
|
|||
return MSR.post<PassRateDataType>({ url: WorkAssociateCaseDetailUrl, data });
|
||||
}
|
||||
|
||||
// 工作台-首页-用例评审数
|
||||
export function workCaseReviewDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<PassRateDataType>({ url: WorkCaseReviewDetailUrl, data });
|
||||
}
|
||||
|
||||
// 工作台-首页-缺陷处理人
|
||||
export function workBugHandlerDetail(data: WorkHomePageDetail) {
|
||||
return MSR.post<OverViewOfProject>({ url: WorkBugHandlerDetailUrl, data });
|
||||
}
|
||||
|
||||
// 工作台-首页-接口变更
|
||||
export function workApiChangeList(data: WorkHomePageDetail) {
|
||||
return MSR.post<CommonList<ApiDefinitionDetail>>(
|
||||
{ url: WorkApiChangeListUrl, data },
|
||||
{ ignoreCancelToken: true, errorMessageMode: 'none' }
|
||||
);
|
||||
}
|
||||
|
||||
// 工作台-首页-接口变更
|
||||
export function workReviewList(data: WorkHomePageDetail) {
|
||||
return MSR.post<CommonList<ReviewItem>>(
|
||||
{ url: WorkReviewListUrl, data },
|
||||
{ ignoreCancelToken: true, errorMessageMode: 'none' }
|
||||
);
|
||||
}
|
||||
|
||||
// 待办-用例评审列表
|
||||
export function workbenchTodoReviewList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<ReviewItem>>({ url: WorkTodoReviewListUrl, data });
|
||||
|
|
|
@ -15,3 +15,7 @@ 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'; // 工作台-首页-关联用例数量
|
||||
export const WorkBugHandlerDetailUrl = '/dashboard/bug_handle_user'; // 工作台-首页-缺陷处理人
|
||||
export const WorkApiChangeListUrl = '/dashboard/api_change'; // 工作台-首页-接口变更
|
||||
export const WorkCaseReviewDetailUrl = '/dashboard/review_case_count'; // 工作台-首页-用例评审数
|
||||
export const WorkReviewListUrl = '/dashboard/reviewing_by_me'; // 工作台-首页-待我评审
|
||||
|
|
|
@ -259,11 +259,12 @@ export default function useTableProps<T>(
|
|||
return data;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO 在这里处理拦截设置表格无资源权限
|
||||
setTableErrorStatus('error');
|
||||
propsRes.value.data = [];
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
|
||||
throw err; // 将错误抛出
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// debug 模式下打印属性
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { commonConfig, toolTipConfig } from '@/config/testPlan';
|
||||
|
||||
import type { ModuleCardItem } from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum, WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
export const contentTabList: ModuleCardItem[] = [
|
||||
{
|
||||
label: 'workbench.homePage.functionalUseCase',
|
||||
value: WorkOverviewEnum.FUNCTIONAL,
|
||||
icon: WorkOverviewIconEnum.FUNCTIONAL,
|
||||
color: 'rgb(var(--primary-5))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.useCaseReview',
|
||||
value: WorkOverviewEnum.CASE_REVIEW,
|
||||
icon: WorkOverviewIconEnum.CASE_REVIEW,
|
||||
color: 'rgb(var(--success-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.interfaceAPI',
|
||||
value: WorkOverviewEnum.API,
|
||||
icon: WorkOverviewIconEnum.API,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.interfaceCASE',
|
||||
value: WorkOverviewEnum.API_CASE,
|
||||
icon: WorkOverviewIconEnum.API_CASE,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.interfaceScenario',
|
||||
value: WorkOverviewEnum.API_SCENARIO,
|
||||
icon: WorkOverviewIconEnum.API_SCENARIO,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.apiPlan',
|
||||
value: WorkOverviewEnum.TEST_PLAN,
|
||||
icon: WorkOverviewIconEnum.TEST_PLAN,
|
||||
color: 'rgb(var(--link-6))',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.bugCount',
|
||||
value: WorkOverviewEnum.BUG_COUNT,
|
||||
icon: WorkOverviewIconEnum.BUG_COUNT,
|
||||
color: 'rgb(var(--danger-6))',
|
||||
count: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// 覆盖率
|
||||
export const defaultCover = [
|
||||
{
|
||||
label: 'workbench.homePage.covered',
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.notCover',
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
// 评审率
|
||||
export const defaultReview = [
|
||||
{
|
||||
label: 'workbench.homePage.reviewed',
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.unReviewed',
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
// 通过率
|
||||
export const defaultPass = [
|
||||
{
|
||||
label: 'workbench.homePage.havePassed',
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.notPass',
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
// 完成率
|
||||
export const defaultComplete = [
|
||||
{
|
||||
label: 'common.completed',
|
||||
value: 10000,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'common.inProgress',
|
||||
value: 2000,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.unFinish',
|
||||
value: 2000,
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
// 执行率
|
||||
export const defaultExecution = [
|
||||
{
|
||||
label: 'common.unExecute',
|
||||
value: 10000,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'common.executed',
|
||||
value: 2000,
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
// 遗留率
|
||||
export const defaultLegacy = [
|
||||
{
|
||||
label: 'workbench.homePage.defectTotal',
|
||||
value: 10000,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: 'workbench.homePage.legacyDefectsNumber',
|
||||
value: 2000,
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultValueMap: Record<string, any> = {
|
||||
// 用例数量
|
||||
[WorkCardEnum.CASE_COUNT]: {
|
||||
review: {
|
||||
defaultList: cloneDeep(defaultCover),
|
||||
color: ['#00C261', '#D4D4D8'],
|
||||
defaultName: 'workbench.homePage.reviewRate',
|
||||
},
|
||||
pass: {
|
||||
defaultList: cloneDeep(defaultPass),
|
||||
color: ['#00C261', '#ED0303'],
|
||||
defaultName: 'workbench.homePage.passRate',
|
||||
},
|
||||
},
|
||||
// 关联用例数量
|
||||
[WorkCardEnum.ASSOCIATE_CASE_COUNT]: {
|
||||
cover: {
|
||||
defaultList: cloneDeep(defaultCover),
|
||||
color: ['#00C261', '#D4D4D8'],
|
||||
defaultName: 'workbench.homePage.coverRate',
|
||||
},
|
||||
},
|
||||
// 用例评审数
|
||||
[WorkCardEnum.REVIEW_CASE_COUNT]: {
|
||||
cover: {
|
||||
defaultList: cloneDeep(defaultCover),
|
||||
color: ['#00C261', '#D4D4D8'],
|
||||
defaultName: 'workbench.homePage.coverRate',
|
||||
},
|
||||
},
|
||||
// 测试计划数
|
||||
[WorkCardEnum.TEST_PLAN_COUNT]: {
|
||||
execute: {
|
||||
defaultList: cloneDeep(defaultExecution),
|
||||
color: ['#D4D4D8', '#00C261'],
|
||||
defaultName: 'workbench.homePage.executeRate',
|
||||
},
|
||||
pass: {
|
||||
defaultList: cloneDeep(defaultPass),
|
||||
color: ['#D4D4D8', '#00C261'],
|
||||
defaultName: 'workbench.homePage.passRate',
|
||||
},
|
||||
complete: {
|
||||
defaultList: cloneDeep(defaultComplete),
|
||||
color: ['#00C261', '#3370FF', '#D4D4D8'],
|
||||
defaultName: 'workbench.homePage.completeRate',
|
||||
},
|
||||
},
|
||||
// 测试计划遗留缺陷
|
||||
[WorkCardEnum.PLAN_LEGACY_BUG]: {
|
||||
legacy: {
|
||||
defaultList: cloneDeep(defaultLegacy),
|
||||
color: ['#D4D4D8', '#00C261'],
|
||||
defaultName: 'workbench.homePage.legacyRate',
|
||||
},
|
||||
},
|
||||
// 缺陷数
|
||||
[WorkCardEnum.BUG_COUNT]: {
|
||||
legacy: {
|
||||
defaultList: cloneDeep(defaultLegacy),
|
||||
color: ['#D4D4D8', '#00C261'],
|
||||
defaultName: 'workbench.homePage.legacyRate',
|
||||
},
|
||||
},
|
||||
// 待我处理的缺陷
|
||||
[WorkCardEnum.HANDLE_BUG_BY_ME]: {
|
||||
legacy: {
|
||||
defaultList: cloneDeep(defaultLegacy),
|
||||
color: ['#D4D4D8', '#00C261'],
|
||||
defaultName: 'workbench.homePage.legacyRate',
|
||||
},
|
||||
},
|
||||
// 接口数量
|
||||
[WorkCardEnum.API_COUNT]: {
|
||||
cover: {
|
||||
defaultList: cloneDeep(defaultCover),
|
||||
color: ['#00C261', '#D4D4D8'],
|
||||
defaultName: 'workbench.homePage.coverRate',
|
||||
},
|
||||
complete: {
|
||||
defaultList: cloneDeep(defaultComplete),
|
||||
color: ['#00C261', '#3370FF', '#D4D4D8'],
|
||||
defaultName: 'workbench.homePage.completeRate',
|
||||
},
|
||||
},
|
||||
// 我创建的缺陷
|
||||
[WorkCardEnum.CREATE_BUG_BY_ME]: {
|
||||
legacy: {
|
||||
defaultList: cloneDeep(defaultLegacy),
|
||||
color: ['#D4D4D8', '#00C261'],
|
||||
defaultName: 'workbench.homePage.legacyRate',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 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: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default {};
|
|
@ -56,7 +56,7 @@ export interface OverViewOfProject {
|
|||
}
|
||||
|
||||
export interface ModuleCardItem {
|
||||
label: string | number;
|
||||
label: string;
|
||||
value: string | number;
|
||||
count?: number;
|
||||
icon?: string;
|
||||
|
@ -73,10 +73,13 @@ export type StatusStatisticsMapType = Record<
|
|||
>;
|
||||
|
||||
export interface PassRateDataType {
|
||||
statusStatisticsMap: StatusStatisticsMapType;
|
||||
statusPercentList: {
|
||||
status: string; // 状态
|
||||
count: number;
|
||||
percentValue: string; // 百分比
|
||||
}[];
|
||||
statusStatisticsMap: StatusStatisticsMapType | null;
|
||||
statusPercentList:
|
||||
| {
|
||||
status: string; // 状态
|
||||
count: number;
|
||||
percentValue: string; // 百分比
|
||||
}[]
|
||||
| null;
|
||||
errorCode: number;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -29,7 +30,16 @@
|
|||
v-on="propsEvent"
|
||||
>
|
||||
<template #num="{ record }">
|
||||
<MsButton type="text">{{ record.num }}</MsButton>
|
||||
<MsButton type="text">{{ record.num || '-' }}</MsButton>
|
||||
</template>
|
||||
<template v-if="isNoPermission" #empty>
|
||||
<div class="w-full">
|
||||
<slot name="empty">
|
||||
<div class="flex h-[40px] flex-col items-center justify-center">
|
||||
<span class="text-[14px] text-[var(--color-text-4)]">{{ t('common.noResource') }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
</MsBaseTable>
|
||||
</div>
|
||||
|
@ -49,6 +59,7 @@
|
|||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
|
||||
import { workApiChangeList } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
|
@ -85,8 +96,8 @@
|
|||
},
|
||||
{
|
||||
title: 'project.commonScript.apiName',
|
||||
slotName: 'apiName',
|
||||
dataIndex: 'apiName',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
|
@ -98,14 +109,14 @@
|
|||
},
|
||||
{
|
||||
title: 'workbench.homePage.associationCASE',
|
||||
slotName: 'case',
|
||||
dataIndex: 'case',
|
||||
slotName: 'caseTotal',
|
||||
dataIndex: 'caseTotal',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'workbench.homePage.associatedScene',
|
||||
slotName: 'associatedScene',
|
||||
dataIndex: 'associatedScene',
|
||||
slotName: 'scenarioTotal',
|
||||
dataIndex: 'scenarioTotal',
|
||||
showDrag: true,
|
||||
width: 100,
|
||||
},
|
||||
|
@ -122,32 +133,43 @@
|
|||
},
|
||||
];
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
|
||||
undefined,
|
||||
workApiChangeList,
|
||||
{
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
selectable: false,
|
||||
heightUsed: 272,
|
||||
showSelectAll: false,
|
||||
validatePermission: true,
|
||||
},
|
||||
(item) => ({
|
||||
...item,
|
||||
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();
|
||||
const isNoPermission = ref<boolean>(false);
|
||||
async function initData() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
setLoadListParams({
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
});
|
||||
await loadList();
|
||||
isNoPermission.value = false;
|
||||
} catch (error) {
|
||||
isNoPermission.value = error === 'no_project_permission';
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
initData();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -160,7 +182,6 @@
|
|||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -170,7 +191,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -24,6 +25,7 @@
|
|||
:tooltip-text="tabItem.tooltip"
|
||||
:options="tabItem.options"
|
||||
:size="60"
|
||||
:has-permission="hasPermission"
|
||||
:value-list="tabItem.valueList"
|
||||
/>
|
||||
</div>
|
||||
|
@ -51,14 +53,9 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type {
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
StatusStatisticsMapType,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
import { handlePieData, handleUpdateTabPie } from '../utils';
|
||||
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
|
@ -82,55 +79,20 @@
|
|||
})
|
||||
);
|
||||
|
||||
const options = ref(cloneDeep(commonRatePieOptions));
|
||||
const options = ref({});
|
||||
|
||||
// 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%' },
|
||||
],
|
||||
statusStatisticsMap: null,
|
||||
statusPercentList: null,
|
||||
errorCode: 109001,
|
||||
});
|
||||
|
||||
const coverValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.covered'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.notCover'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const passValueList = ref([
|
||||
{
|
||||
label: t('common.completed'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('common.inProgress'),
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.unFinish'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const coverOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const completeOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const coverValueList = ref<{ value: string | number; label: string; name: string }[]>([]);
|
||||
|
||||
const passValueList = ref<{ value: string | number; label: string; name: string }[]>([]);
|
||||
const coverOptions = ref<Record<string, any>>({});
|
||||
const completeOptions = ref<Record<string, any>>({});
|
||||
const apiCountTabList = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
@ -152,34 +114,7 @@
|
|||
|
||||
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'];
|
||||
}
|
||||
|
||||
const hasPermission = ref<boolean>(false);
|
||||
function initApiCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
|
@ -193,14 +128,38 @@
|
|||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
apiCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
const { statusStatisticsMap, statusPercentList, errorCode } = detail.value;
|
||||
|
||||
hasPermission.value = errorCode !== 109001;
|
||||
|
||||
apiCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
|
||||
|
||||
// 覆盖率
|
||||
const { options: covOptions, valueList: coverList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.cover || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-cover`
|
||||
);
|
||||
coverValueList.value = coverList;
|
||||
coverOptions.value = covOptions;
|
||||
|
||||
const { options: comOptions, valueList: completedList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.cover || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-complete`
|
||||
);
|
||||
passValueList.value = completedList;
|
||||
completeOptions.value = comOptions;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
initApiCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initApiCount();
|
||||
});
|
||||
|
@ -211,7 +170,6 @@
|
|||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -221,7 +179,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -25,6 +26,7 @@
|
|||
:tooltip-text="tabItem.tooltip"
|
||||
:size="60"
|
||||
:value-list="tabItem.valueList"
|
||||
:has-permission="hasPermission"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -41,7 +43,6 @@
|
|||
* @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';
|
||||
|
@ -53,9 +54,8 @@
|
|||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
import { StatusStatisticsMapType } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
import { handlePieData, handleUpdateTabPie } from '../utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
@ -79,42 +79,12 @@
|
|||
})
|
||||
);
|
||||
|
||||
const options = ref(cloneDeep(commonRatePieOptions));
|
||||
const reviewValueList = ref<{ value: number | string; label: string; name: string }[]>([]);
|
||||
|
||||
function handlePassRatePercent(data: { name: string; count: number }[]) {
|
||||
return data.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
const passValueList = ref<{ value: number | string; label: string; name: string }[]>([]);
|
||||
|
||||
const reviewValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.reviewed'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.unReviewed'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
|
||||
const passValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.havePassed'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.notPass'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
|
||||
const reviewOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const passOptions = ref<Record<string, any>>(cloneDeep(options.value));
|
||||
const reviewOptions = ref<Record<string, any>>({});
|
||||
const passOptions = ref<Record<string, any>>({});
|
||||
const caseCountTabList = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
@ -134,24 +104,10 @@
|
|||
];
|
||||
});
|
||||
|
||||
// 处理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 hasPermission = ref<boolean>(false);
|
||||
|
||||
const caseCountOptions = ref<Record<string, any>>({});
|
||||
|
||||
async function initCaseCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
|
@ -167,14 +123,35 @@
|
|||
};
|
||||
const detail = await workCaseCountDetail(params);
|
||||
const { statusStatisticsMap, statusPercentList } = detail;
|
||||
caseCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
hasPermission.value = detail.errorCode !== 109001;
|
||||
caseCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
|
||||
|
||||
const { valueList: reviewValue, options: reviewedOptions } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.review || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-review`
|
||||
);
|
||||
|
||||
reviewOptions.value = reviewedOptions;
|
||||
reviewValueList.value = reviewValue;
|
||||
|
||||
const { valueList: passList, options: passOpt } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.pass || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-pass`
|
||||
);
|
||||
passOptions.value = passOpt;
|
||||
passValueList.value = passList;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
initCaseCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCaseCount();
|
||||
});
|
||||
|
@ -185,7 +162,6 @@
|
|||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initCaseCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -195,7 +171,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initCaseCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -24,6 +25,7 @@
|
|||
tooltip-text="workbench.homePage.caseReviewCoverRateTooltip"
|
||||
:size="60"
|
||||
:value-list="coverValueList"
|
||||
:has-permission="hasPermission"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,18 +41,18 @@
|
|||
* @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 { workCaseReviewDetail } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
import { handlePieData, handleUpdateTabPie } from '../utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
@ -74,54 +76,58 @@
|
|||
})
|
||||
);
|
||||
|
||||
const options = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
|
||||
const options = ref<Record<string, any>>({});
|
||||
|
||||
const coverValueList = ref([
|
||||
const coverValueList = ref<{ value: number | string; label: string; name: string }[]>([
|
||||
{
|
||||
label: t('workbench.homePage.covered'),
|
||||
value: 10000,
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.notCover'),
|
||||
value: 2000,
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
]);
|
||||
|
||||
// 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,
|
||||
};
|
||||
});
|
||||
const hasPermission = ref<boolean>(false);
|
||||
async function initApiCount() {
|
||||
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: [],
|
||||
};
|
||||
try {
|
||||
const detail: PassRateDataType = await workCaseReviewDetail(params);
|
||||
|
||||
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'];
|
||||
hasPermission.value = detail.errorCode !== 109001;
|
||||
|
||||
const { statusStatisticsMap, statusPercentList } = detail;
|
||||
caseReviewCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
|
||||
const { options: coverOptions, valueList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.cover || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-cover`
|
||||
);
|
||||
coverValueList.value = valueList;
|
||||
options.value = coverOptions;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
initApiCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -134,7 +140,6 @@
|
|||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -144,7 +149,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initApiCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -19,7 +20,13 @@
|
|||
<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" />
|
||||
<PassRatePie
|
||||
:has-permission="hasPermission"
|
||||
:tooltip-text="tooltip"
|
||||
:options="legacyOptions"
|
||||
:size="60"
|
||||
:value-list="legacyValueList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[148px]">
|
||||
|
@ -34,7 +41,6 @@
|
|||
* @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';
|
||||
|
@ -43,15 +49,10 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type {
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
StatusStatisticsMapType,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
import { handlePieData, handleUpdateTabPie } from '../utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
|
@ -67,21 +68,7 @@
|
|||
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const valueList = ref<
|
||||
{
|
||||
label: string;
|
||||
value: number;
|
||||
}[]
|
||||
>([
|
||||
{
|
||||
label: t('workbench.homePage.defectTotal'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.legacyDefectsNumber'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const legacyValueList = ref<{ value: number | string; label: string; name: string }[]>([]);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
|
@ -92,7 +79,7 @@
|
|||
})
|
||||
);
|
||||
|
||||
const legacyOptions = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
|
||||
const legacyOptions = ref<Record<string, any>>({});
|
||||
|
||||
// TODO 假数据
|
||||
const detail = ref<PassRateDataType>({
|
||||
|
@ -108,26 +95,12 @@
|
|||
{ status: 'BBB', count: 3, percentValue: '0%' },
|
||||
{ status: 'CCC', count: 6, percentValue: '0%' },
|
||||
],
|
||||
errorCode: 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'];
|
||||
}
|
||||
const countOptions = ref<Record<string, any>>({});
|
||||
|
||||
const hasPermission = ref<boolean>(false);
|
||||
async function initCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
|
@ -141,9 +114,18 @@
|
|||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
countOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
const { statusStatisticsMap, statusPercentList, errorCode } = detail.value;
|
||||
hasPermission.value = errorCode !== 109001;
|
||||
|
||||
countOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
|
||||
|
||||
const { options, valueList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.legacy || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-legacy`
|
||||
);
|
||||
legacyValueList.value = valueList;
|
||||
legacyOptions.value = options;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -154,6 +136,10 @@
|
|||
return props.item.key === WorkCardEnum.PLAN_LEGACY_BUG ? 'workbench.homePage.planCaseCountLegacyRateTooltip' : '';
|
||||
});
|
||||
|
||||
function changeProject() {
|
||||
initCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCount();
|
||||
});
|
||||
|
@ -164,7 +150,6 @@
|
|||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -174,7 +159,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
<MsSelect
|
||||
v-model:model-value="projectId"
|
||||
:options="appStore.projectList"
|
||||
allow-clear
|
||||
allow-search
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
<MsSelect
|
||||
|
@ -25,6 +25,7 @@
|
|||
:multiple="true"
|
||||
:has-all-select="true"
|
||||
:default-all-select="true"
|
||||
@change="changeMember"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -44,12 +45,15 @@
|
|||
import MsChart from '@/components/pure/chart/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
|
||||
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
|
||||
import { workBugHandlerDetail } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { characterLimit } from '@/utils';
|
||||
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
import type { OverViewOfProject, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonColorConfig, getCommonBarOptions } from '../utils';
|
||||
import { commonColorConfig, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -58,19 +62,18 @@
|
|||
item: SelectedCardItem;
|
||||
}>();
|
||||
|
||||
const memberIds = ref('');
|
||||
const innerProjectIds = defineModel<string[]>('projectIds', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const projectId = computed<string>({
|
||||
get: () => {
|
||||
const [newProject] = innerProjectIds.value;
|
||||
return newProject;
|
||||
},
|
||||
set: (val) => val,
|
||||
const projectId = ref<string>(innerProjectIds.value[0]);
|
||||
|
||||
const innerHandleUsers = defineModel<string[]>('handleUsers', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const memberIds = ref<string[]>(innerHandleUsers.value);
|
||||
|
||||
const timeForm = inject<Ref<TimeFormParams>>(
|
||||
'timeForm',
|
||||
ref({
|
||||
|
@ -83,128 +86,91 @@
|
|||
const memberOptions = ref<SelectOptionData[]>([]);
|
||||
|
||||
const options = ref<Record<string, any>>({});
|
||||
const members = computed(() => ['张三', '李四', '王五', '小王']);
|
||||
const hasRoom = computed(() => members.value.length >= 7);
|
||||
const seriesData = ref<Record<string, any>[]>([
|
||||
{
|
||||
name: '新创建',
|
||||
type: 'bar',
|
||||
barWidth: 12,
|
||||
stack: 'bug',
|
||||
itemStyle: {
|
||||
// borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
data: [400, 200, 400, 200, 400, 200],
|
||||
},
|
||||
{
|
||||
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'];
|
||||
|
||||
function getDefectMemberDetail() {
|
||||
options.value = getCommonBarOptions(hasRoom.value, [...defectStatusColor, ...commonColorConfig]);
|
||||
options.value.xAxis.data = members.value;
|
||||
options.value.series = seriesData.value;
|
||||
function handleData(detail: OverViewOfProject) {
|
||||
options.value = getCommonBarOptions(detail.xaxis.length >= 7, [...defectStatusColor, ...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) => {
|
||||
return {
|
||||
name: item.name,
|
||||
type: 'bar',
|
||||
stack: 'bugMember',
|
||||
barWidth: 12,
|
||||
data: item.count,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
async function getDefectMemberDetail() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const detail = await workBugHandlerDetail({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: innerHandleUsers.value,
|
||||
});
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
memberIds.value = [];
|
||||
getMemberOptions();
|
||||
getDefectMemberDetail();
|
||||
});
|
||||
}
|
||||
|
||||
function changeMember() {
|
||||
getDefectMemberDetail();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => projectId.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
getDefectMemberDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => memberIds.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerHandleUsers.value = val;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -220,6 +186,11 @@
|
|||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getMemberOptions();
|
||||
getDefectMemberDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
import TabCard from './tabCard.vue';
|
||||
|
||||
import { workMyCreatedDetail, workProOverviewDetail } from '@/api/modules/workbench';
|
||||
import { contentTabList } from '@/config/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
|
@ -53,7 +54,7 @@
|
|||
} from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum, WorkOverviewEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
import { commonColorConfig, contentTabList, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
import { commonColorConfig, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -92,9 +93,10 @@
|
|||
function handleData(detail: OverViewOfProject) {
|
||||
// 处理模块顺序
|
||||
const tempAxisData = detail.xaxis.map((xAxisKey) => {
|
||||
const data = contentTabList.value.find((e) => e.value === xAxisKey);
|
||||
const data = contentTabList.find((e) => e.value === xAxisKey);
|
||||
return {
|
||||
...data,
|
||||
label: t(data?.label || ''),
|
||||
count: detail.caseCountMap[xAxisKey as WorkOverviewEnum],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
<MsSelect
|
||||
|
@ -48,13 +49,14 @@
|
|||
|
||||
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
|
||||
import { workMemberViewDetail } from '@/api/modules/workbench';
|
||||
import { contentTabList } from '@/config/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { characterLimit } from '@/utils';
|
||||
|
||||
import type { OverViewOfProject, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonColorConfig, contentTabList, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
import { commonColorConfig, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
@ -82,19 +84,18 @@
|
|||
endTime: 0,
|
||||
})
|
||||
);
|
||||
const hasRoom = computed(() => memberIds.value.length >= 7);
|
||||
const memberOptions = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
const memberOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const options = ref<Record<string, any>>({});
|
||||
function handleData(detail: OverViewOfProject) {
|
||||
options.value = getCommonBarOptions(hasRoom.value, commonColorConfig);
|
||||
options.value = getCommonBarOptions(detail.xaxis.length >= 7, 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,
|
||||
name: t(contentTabList[index].label),
|
||||
type: 'bar',
|
||||
stack: 'member',
|
||||
barWidth: 12,
|
||||
|
@ -122,6 +123,7 @@
|
|||
const detail = await workMemberViewDetail(params);
|
||||
handleData(detail);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +137,11 @@
|
|||
}));
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
getMemberOptions();
|
||||
initOverViewMemberDetail();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => innerProjectIds.value,
|
||||
(val) => {
|
||||
|
@ -142,8 +149,6 @@
|
|||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
memberIds.value = [];
|
||||
getMemberOptions();
|
||||
initOverViewMemberDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -181,6 +186,7 @@
|
|||
|
||||
onMounted(() => {
|
||||
getMemberOptions();
|
||||
initOverViewMemberDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<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>
|
||||
<div class="pass-rate-count">{{ addCommasToNumber(item.value) }}</div>
|
||||
<div class="pass-rate-count">{{ hasPermission ? addCommasToNumber(item.value as number) : '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,9 +34,10 @@
|
|||
options: Record<string, any>;
|
||||
size: number;
|
||||
tooltipText?: string;
|
||||
hasPermission: boolean;
|
||||
valueList: {
|
||||
label: string;
|
||||
value: number;
|
||||
value: number | string;
|
||||
}[];
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
name: '',
|
||||
type: 'pie',
|
||||
padAngle: 1,
|
||||
radius: ['46%', '56%'],
|
||||
radius: ['50%', '58%'],
|
||||
center: ['50%', '32%'],
|
||||
color: [],
|
||||
avoidLabelOverlap: false,
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -19,7 +20,8 @@
|
|||
<div class="case-count-wrapper">
|
||||
<div class="case-count-item">
|
||||
<PassRatePie
|
||||
:options="options"
|
||||
:options="relatedOptions"
|
||||
:has-permission="hasPermission"
|
||||
tooltip-text="workbench.homePage.associateCaseCoverRateTooltip"
|
||||
:size="60"
|
||||
:value-list="coverRateValueList"
|
||||
|
@ -35,7 +37,6 @@
|
|||
* @desc 关联用例数量
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import PassRatePie from './passRatePie.vue';
|
||||
|
@ -44,9 +45,9 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions } from '../utils';
|
||||
import { handleUpdateTabPie } from '../utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
|
@ -71,14 +72,26 @@
|
|||
})
|
||||
);
|
||||
|
||||
const options = ref<Record<string, any>>(cloneDeep(commonRatePieOptions));
|
||||
const relatedOptions = ref<Record<string, any>>({});
|
||||
const hasPermission = ref<boolean>(false);
|
||||
|
||||
const coverRateValueList = ref<{ value: number; label: string; name: string }[]>([]);
|
||||
const coverRateValueList = ref<{ value: number | string; label: string; name: string }[]>([
|
||||
{
|
||||
label: t('workbench.homePage.covered'),
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.notCover'),
|
||||
value: '-',
|
||||
name: '',
|
||||
},
|
||||
]);
|
||||
|
||||
async function getRelatedCaseCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
const detail = await workAssociateCaseDetail({
|
||||
const detail: PassRateDataType = await workAssociateCaseDetail({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
startTime: dayNumber ? null : startTime,
|
||||
|
@ -88,24 +101,28 @@
|
|||
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'];
|
||||
|
||||
hasPermission.value = detail.errorCode !== 109001;
|
||||
|
||||
const { statusStatisticsMap } = detail;
|
||||
|
||||
const { options, valueList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.cover || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-cover`
|
||||
);
|
||||
relatedOptions.value = options;
|
||||
coverRateValueList.value = valueList;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
getRelatedCaseCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRelatedCaseCount();
|
||||
});
|
||||
|
@ -125,7 +142,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
getRelatedCaseCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
const props = defineProps<{
|
||||
contentTabList: {
|
||||
label: string | number;
|
||||
label: string;
|
||||
value: string | number;
|
||||
count?: number;
|
||||
icon?: string;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -22,7 +23,12 @@
|
|||
<TabCard :content-tab-list="testPlanTabList" not-has-padding hidden-border min-width="270px">
|
||||
<template #item="{ item: tabItem }">
|
||||
<div class="w-full">
|
||||
<PassRatePie :options="tabItem.options" :size="60" :value-list="tabItem.valueList" />
|
||||
<PassRatePie
|
||||
:has-permission="hasPermission"
|
||||
:options="tabItem.options"
|
||||
:size="60"
|
||||
:value-list="tabItem.valueList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TabCard>
|
||||
|
@ -38,7 +44,6 @@
|
|||
* @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';
|
||||
|
@ -48,14 +53,9 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type {
|
||||
PassRateDataType,
|
||||
SelectedCardItem,
|
||||
StatusStatisticsMapType,
|
||||
TimeFormParams,
|
||||
} from '@/models/workbench/homePage';
|
||||
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||
|
||||
import { commonRatePieOptions, handlePieData } from '../utils';
|
||||
import { handlePieData, handleUpdateTabPie } from '../utils';
|
||||
|
||||
const props = defineProps<{
|
||||
item: SelectedCardItem;
|
||||
|
@ -103,51 +103,20 @@
|
|||
{ status: 'TCP', count: 3, percentValue: '0%' },
|
||||
{ status: 'BBB', count: 6, percentValue: '0%' },
|
||||
],
|
||||
errorCode: 109001,
|
||||
});
|
||||
|
||||
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 executionOptions = ref<Record<string, any>>({});
|
||||
const passOptions = ref<Record<string, any>>({});
|
||||
const completeOptions = ref<Record<string, any>>({});
|
||||
|
||||
// 执行率
|
||||
const executionValueList = ref([
|
||||
{
|
||||
label: t('common.unExecute'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('common.executed'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const executionValueList = ref<{ value: number | string; label: string; name: string }[]>([]);
|
||||
// 通过率
|
||||
const passValueList = ref([
|
||||
{
|
||||
label: t('workbench.homePage.havePassed'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.notPass'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const passValueList = ref<{ value: number | string; label: string; name: string }[]>([]);
|
||||
|
||||
// 完成率
|
||||
const completeValueList = ref([
|
||||
{
|
||||
label: t('common.completed'),
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: t('common.inProgress'),
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
label: t('workbench.homePage.unFinish'),
|
||||
value: 2000,
|
||||
},
|
||||
]);
|
||||
const completeValueList = ref<{ value: number | string; label: string; name: string }[]>([]);
|
||||
|
||||
const testPlanTabList = computed(() => {
|
||||
return [
|
||||
|
@ -172,41 +141,8 @@
|
|||
];
|
||||
});
|
||||
|
||||
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({});
|
||||
const hasPermission = ref<boolean>(false);
|
||||
async function initTestPlanCount() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
|
@ -220,18 +156,47 @@
|
|||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
};
|
||||
const { statusStatisticsMap, statusPercentList } = detail.value;
|
||||
const { statusStatisticsMap, statusPercentList, errorCode } = detail.value;
|
||||
|
||||
testPlanCountOptions.value = handlePieData(props.item.key, statusPercentList);
|
||||
handleRatePieData(statusStatisticsMap);
|
||||
hasPermission.value = errorCode !== 109001;
|
||||
testPlanCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
|
||||
|
||||
// 执行率
|
||||
const { options: executedOptions, valueList: executedList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.execute || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-execute`
|
||||
);
|
||||
|
||||
// 通过率
|
||||
const { options: passedOptions, valueList: passList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.pass || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-pass`
|
||||
);
|
||||
|
||||
// 完成率
|
||||
const { options: comOptions, valueList: completeList } = handleUpdateTabPie(
|
||||
statusStatisticsMap?.complete || [],
|
||||
hasPermission.value,
|
||||
`${props.item.key}-complete`
|
||||
);
|
||||
|
||||
executionValueList.value = executedList;
|
||||
passValueList.value = passList;
|
||||
completeValueList.value = completeList;
|
||||
|
||||
executionOptions.value = executedOptions;
|
||||
passOptions.value = passedOptions;
|
||||
completeOptions.value = comOptions;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
function changeProject() {
|
||||
initTestPlanCount();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTestPlanCount();
|
||||
|
@ -242,7 +207,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initTestPlanCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -253,7 +217,6 @@
|
|||
if (val) {
|
||||
const [newProjectId] = val;
|
||||
projectId.value = newProjectId;
|
||||
initTestPlanCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:search-keys="['name']"
|
||||
class="!w-[240px]"
|
||||
:prefix="t('workbench.homePage.project')"
|
||||
@change="changeProject"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
|
@ -57,6 +58,15 @@
|
|||
}}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="isNoPermission" #empty>
|
||||
<div class="w-full">
|
||||
<slot name="empty">
|
||||
<div class="flex h-[40px] flex-col items-center justify-center">
|
||||
<span class="text-[14px] text-[var(--color-text-4)]">{{ t('common.noResource') }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
</MsBaseTable>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,6 +84,7 @@
|
|||
import MsSelect from '@/components/business/ms-select';
|
||||
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
|
||||
|
||||
import { workReviewList } from '@/api/modules/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
|
@ -130,7 +141,7 @@
|
|||
},
|
||||
];
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(undefined, {
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(workReviewList, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
selectable: false,
|
||||
|
@ -138,19 +149,28 @@
|
|||
showSelectAll: false,
|
||||
});
|
||||
|
||||
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();
|
||||
const isNoPermission = ref<boolean>(false);
|
||||
|
||||
async function initData() {
|
||||
try {
|
||||
const { startTime, endTime, dayNumber } = timeForm.value;
|
||||
setLoadListParams({
|
||||
startTime: dayNumber ? null : startTime,
|
||||
endTime: dayNumber ? null : endTime,
|
||||
dayNumber: dayNumber ?? null,
|
||||
projectIds: innerProjectIds.value,
|
||||
organizationId: appStore.currentOrgId,
|
||||
handleUsers: [],
|
||||
});
|
||||
await loadList();
|
||||
} catch (error) {
|
||||
isNoPermission.value = error === 'no_project_permission';
|
||||
// eslint-disable-next-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
function changeProject() {
|
||||
initData();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -162,7 +182,6 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
innerProjectIds.value = [val];
|
||||
initData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
<DefectMemberBar
|
||||
v-else-if="item.key === WorkCardEnum.BUG_HANDLE_USER"
|
||||
v-model:projectIds="item.projectIds"
|
||||
v-model:handleUsers="item.handleUsers"
|
||||
:item="item"
|
||||
/>
|
||||
<DefectCount
|
||||
|
|
|
@ -106,4 +106,10 @@ export default {
|
|||
'Scenario Pass Rate: Last successful execution scenarios / Total scenarios * 100%',
|
||||
'workbench.homePage.planCaseCountLegacyRateTooltip':
|
||||
'Legacy Rate: Unresolved defects / All associated defects * 100%',
|
||||
'workbench.homePage.reviewRate': 'Review Rate',
|
||||
'workbench.homePage.passRate': 'Pass Rate',
|
||||
'workbench.homePage.coverRate': 'Coverage Rate',
|
||||
'workbench.homePage.executeRate': 'Execution Rate',
|
||||
'workbench.homePage.completeRate': 'Completion Rate',
|
||||
'workbench.homePage.legacyRate': 'Legacy Rate',
|
||||
};
|
||||
|
|
|
@ -92,4 +92,10 @@ export default {
|
|||
'workbench.homePage.scenarioCaseCountExecuteRateTooltip': '场景执行率:执行过的场景/所有场景 * 100%',
|
||||
'workbench.homePage.scenarioCaseCountPassRateTooltip': '场景通过率:最后一次执行成功的场景/场景总数*100%',
|
||||
'workbench.homePage.planCaseCountLegacyRateTooltip': '遗留率:未关闭缺陷/所有关联的缺陷*100%',
|
||||
'workbench.homePage.reviewRate': '评审率',
|
||||
'workbench.homePage.passRate': '通过率',
|
||||
'workbench.homePage.coverRate': '覆盖率',
|
||||
'workbench.homePage.executeRate': '执行率',
|
||||
'workbench.homePage.completeRate': '完成率',
|
||||
'workbench.homePage.legacyRate': '遗留率',
|
||||
};
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { commonConfig, toolTipConfig } from '@/config/testPlan';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { toolTipConfig } from '@/config/testPlan';
|
||||
import { commonRatePieOptions, defaultValueMap } from '@/config/workbench';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { ModuleCardItem } from '@/models/workbench/homePage';
|
||||
import { WorkCardEnum, WorkOverviewEnum, WorkOverviewIconEnum } from '@/enums/workbenchEnum';
|
||||
import { WorkCardEnum } from '@/enums/workbenchEnum';
|
||||
|
||||
const { t } = useI18n();
|
||||
// 通用颜色配置
|
||||
|
@ -37,7 +39,7 @@ export const commonColorConfig = [
|
|||
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.REVIEW_CASE_COUNT]: ['#9441B1', '#00C261', '#D4D4D8', '#3370FF'],
|
||||
[WorkCardEnum.TEST_PLAN_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.PLAN_LEGACY_BUG]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
||||
[WorkCardEnum.BUG_COUNT]: ['#FFA200', '#00C261', '#D4D4D8'],
|
||||
|
@ -100,13 +102,14 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
],
|
||||
color,
|
||||
grid: {
|
||||
top: '36px',
|
||||
left: '10px',
|
||||
right: '10px',
|
||||
bottom: hasRoom ? '54px' : '5px',
|
||||
top: 36,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: hasRoom ? 54 : 5,
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
show: true,
|
||||
splitLine: false,
|
||||
boundaryGap: true,
|
||||
type: 'category',
|
||||
|
@ -128,7 +131,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
{
|
||||
type: 'value',
|
||||
name: '单位:个', // 设置单位
|
||||
nameLocation: 'end',
|
||||
position: 'left',
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#AEAEB2', // 自定义字体大小和颜色
|
||||
|
@ -143,6 +146,8 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
type: 'dashed', // 水平线线型,可选 'solid'、'dashed'、'dotted'
|
||||
},
|
||||
},
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
],
|
||||
graphic: {
|
||||
|
@ -159,7 +164,6 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
},
|
||||
invisible: true,
|
||||
},
|
||||
|
||||
colorBy: 'series',
|
||||
series: [],
|
||||
barCategoryGap: '50%', // 控制 X 轴分布居中效果
|
||||
|
@ -194,60 +198,8 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
|
|||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
export function getPieCharOptions(key: WorkCardEnum, hasPermission: boolean) {
|
||||
return {
|
||||
title: {
|
||||
show: true,
|
||||
|
@ -272,6 +224,7 @@ export function getPieCharOptions(key: WorkCardEnum) {
|
|||
tooltip: {
|
||||
...toolTipConfig,
|
||||
position: 'right',
|
||||
show: !!hasPermission,
|
||||
},
|
||||
legend: {
|
||||
width: '100%',
|
||||
|
@ -387,6 +340,20 @@ export function getPieCharOptions(key: WorkCardEnum) {
|
|||
},
|
||||
data: [],
|
||||
},
|
||||
graphic: {
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
style: {
|
||||
text: t('workbench.homePage.notHasResPermission'),
|
||||
fontSize: 14,
|
||||
fill: '#959598',
|
||||
backgroundColor: '#F9F9FE',
|
||||
padding: [6, 16, 6, 16],
|
||||
borderRadius: 4,
|
||||
},
|
||||
invisible: !!hasPermission,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -418,108 +385,21 @@ export function handleNoDataDisplay(
|
|||
};
|
||||
}
|
||||
|
||||
// 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; // 百分比
|
||||
}[]
|
||||
hasPermission: boolean,
|
||||
statusPercentList:
|
||||
| {
|
||||
status: string; // 状态
|
||||
count: number;
|
||||
percentValue: string; // 百分比
|
||||
}[]
|
||||
| null = []
|
||||
) {
|
||||
const options: Record<string, any> = getPieCharOptions(key);
|
||||
options.series.data = statusPercentList.map((item) => ({
|
||||
const options: Record<string, any> = getPieCharOptions(key, hasPermission);
|
||||
const lastStatusPercentList = statusPercentList ?? [];
|
||||
options.series.data = lastStatusPercentList.map((item) => ({
|
||||
name: item.status,
|
||||
value: item.count,
|
||||
}));
|
||||
|
@ -527,11 +407,17 @@ export function handlePieData(
|
|||
// 计算总数和图例格式
|
||||
const tempObject: Record<string, any> = {};
|
||||
let totalCount = 0;
|
||||
statusPercentList.forEach((item) => {
|
||||
lastStatusPercentList.forEach((item) => {
|
||||
tempObject[item.status] = item;
|
||||
totalCount += item.count;
|
||||
});
|
||||
|
||||
// 设置副标题为总数
|
||||
options.title.subtext = addCommasToNumber(totalCount);
|
||||
if (!hasPermission) {
|
||||
options.title.subtext = '-';
|
||||
}
|
||||
|
||||
// 设置图例的格式化函数,显示百分比
|
||||
options.legend.formatter = (name: string) => {
|
||||
return `{a|${tempObject[name].status}} {b|${addCommasToNumber(tempObject[name].count)}} {c|${
|
||||
|
@ -539,8 +425,51 @@ export function handlePieData(
|
|||
}}`;
|
||||
};
|
||||
|
||||
// 设置副标题为总数
|
||||
options.title.subtext = addCommasToNumber(totalCount);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// 更新options
|
||||
export function handleUpdateTabPie(
|
||||
list: {
|
||||
name: string;
|
||||
count: number;
|
||||
}[],
|
||||
hasPermission: boolean, // 是否有权限
|
||||
key: string
|
||||
) {
|
||||
const options: Record<string, any> = cloneDeep(commonRatePieOptions);
|
||||
const typeKey = key.split('-')[0];
|
||||
const valueKey = key.split('-')[1];
|
||||
const countList = list || [];
|
||||
let lastCountList: { value: number | string; label: string; name: string }[] = [];
|
||||
if (hasPermission) {
|
||||
lastCountList = countList.slice(1).map((item) => {
|
||||
return {
|
||||
value: item.count,
|
||||
label: item.name,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
options.series.data = lastCountList;
|
||||
|
||||
options.title.text = countList[0].name ?? '';
|
||||
options.title.subtext = `${countList[0].count ?? 0}%`;
|
||||
} else {
|
||||
options.series.data = [];
|
||||
lastCountList = defaultValueMap[typeKey][valueKey].defaultList.map((e: any) => {
|
||||
return {
|
||||
...e,
|
||||
label: t(e.label),
|
||||
};
|
||||
});
|
||||
options.title.text = t(defaultValueMap[typeKey][valueKey].defaultName);
|
||||
options.title.subtext = '-%';
|
||||
}
|
||||
|
||||
options.series.color = defaultValueMap[typeKey][valueKey].color;
|
||||
|
||||
return {
|
||||
valueList: lastCountList,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue