feat(工作台): 优化饼图图例自适应图例

This commit is contained in:
xinxin.wu 2024-11-28 17:38:04 +08:00 committed by Craftsman
parent 8d399692b2
commit fd05fdaa35
11 changed files with 286 additions and 141 deletions

View File

@ -1,5 +1,5 @@
<template>
<VCharts v-if="renderChart" :option="options" :autoresize="autoResize" :style="{ width, height }" />
<VCharts v-if="renderChart" ref="chartRef" :option="options" :autoresize="autoResize" :style="{ width, height }" />
</template>
<script lang="ts" setup>
@ -55,7 +55,13 @@
});
const renderChart = ref(false);
const chartRef = ref<InstanceType<typeof VCharts>>();
nextTick(() => {
renderChart.value = true;
});
defineExpose({
chartRef,
});
</script>

View File

@ -38,8 +38,13 @@
/>
</div>
</div>
<div class="h-[148px]">
<MsChart :options="apiCountOptions" />
<div class="mt-[16px] h-[148px]">
<LegendPieChart
v-model:currentPage="currentPage"
:has-permission="hasPermission"
:data="statusPercentValue"
:options="apiCountOptions"
/>
</div>
</div>
</div>
@ -52,9 +57,9 @@
*/
import { ref } from 'vue';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import CardSkeleton from './cardSkeleton.vue';
import LegendPieChart, { legendDataType } from './legendPieChart.vue';
import PassRatePie from './passRatePie.vue';
import { workApiCountCoverRage, workApiCountDetail } from '@/api/modules/workbench';
@ -63,7 +68,7 @@
import type { ApiCoverageData, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { handlePieData, handleUpdateTabPie } from '../utils';
import { colorMapConfig, handlePieData, handleUpdateTabPie } from '../utils';
const { t } = useI18n();
const appStore = useAppStore();
@ -87,6 +92,7 @@
const loading = ref<boolean>(false);
const projectId = ref<string>(innerProjectIds.value[0]);
const currentPage = ref(1);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
@ -123,6 +129,9 @@
const apiCountOptions = ref({});
const hasPermission = ref<boolean>(false);
const statusPercentValue = ref<legendDataType[]>([]);
async function handleCoverData(detail: ApiCoverageData) {
const { unCoverWithApiDefinition, coverWithApiDefinition, apiCoverage } = detail;
const coverData: {
@ -150,6 +159,7 @@
coverValueList.value = [...coverList];
coverOptions.value = { ...covOptions };
}
async function initApiCountRate() {
try {
loading.value = true;
@ -181,6 +191,13 @@
handleUsers: [],
});
const { statusStatisticsMap, statusPercentList, errorCode } = detail;
statusPercentValue.value = (statusPercentList || []).map((item, index) => {
return {
...item,
selected: true,
color: `${colorMapConfig[props.item.key][index]}`,
};
});
hasPermission.value = errorCode !== 109001;
apiCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);

View File

@ -34,8 +34,13 @@
</div>
</div>
<div class="h-[148px]">
<MsChart :options="caseCountOptions" />
<div class="mt-[16px] h-[148px]">
<LegendPieChart
v-model:currentPage="currentPage"
:has-permission="hasPermission"
:data="statusPercentValue"
:options="caseCountOptions"
/>
</div>
</div>
</div>
@ -48,9 +53,9 @@
*/
import { ref } from 'vue';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import CardSkeleton from './cardSkeleton.vue';
import LegendPieChart, { legendDataType } from './legendPieChart.vue';
import PassRatePie from './passRatePie.vue';
import { workCaseCountDetail } from '@/api/modules/workbench';
@ -59,7 +64,7 @@
import type { SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { handlePieData, handleUpdateTabPie } from '../utils';
import { colorMapConfig, handlePieData, handleUpdateTabPie } from '../utils';
const appStore = useAppStore();
const { t } = useI18n();
@ -78,6 +83,7 @@
});
const projectId = ref<string>(innerProjectIds.value[0]);
const currentPage = ref(1);
const timeForm = inject<Ref<TimeFormParams>>(
'timeForm',
@ -117,6 +123,7 @@
const caseCountOptions = ref<Record<string, any>>({});
const showSkeleton = ref(false);
const statusPercentValue = ref<legendDataType[]>([]);
async function initCaseCount() {
try {
@ -134,6 +141,14 @@
};
const detail = await workCaseCountDetail(params);
const { statusStatisticsMap, statusPercentList } = detail;
statusPercentValue.value = (statusPercentList || []).map((item, index) => {
return {
...item,
selected: true,
color: `${colorMapConfig[props.item.key][index]}`,
};
});
hasPermission.value = detail.errorCode !== 109001;
caseCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);

View File

@ -34,7 +34,12 @@
</div>
</div>
<div class="h-[148px]">
<MsChart :options="caseReviewCountOptions" />
<LegendPieChart
v-model:currentPage="currentPage"
:has-permission="hasPermission"
:data="statusPercentValue"
:options="caseReviewCountOptions"
/>
</div>
</div>
</div>
@ -47,9 +52,9 @@
*/
import { ref } from 'vue';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import CardSkeleton from './cardSkeleton.vue';
import LegendPieChart, { legendDataType } from './legendPieChart.vue';
import PassRatePie from './passRatePie.vue';
import { workCaseReviewDetail } from '@/api/modules/workbench';
@ -58,7 +63,7 @@
import type { PassRateDataType, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
import { handlePieData, handleUpdateTabPie } from '../utils';
import { colorMapConfig, handlePieData, handleUpdateTabPie } from '../utils';
const { t } = useI18n();
const appStore = useAppStore();
@ -75,6 +80,7 @@
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const currentPage = ref(1);
const projectId = ref<string>(innerProjectIds.value[0]);
@ -106,6 +112,7 @@
const hasPermission = ref<boolean>(false);
const showSkeleton = ref(false);
const statusPercentValue = ref<legendDataType[]>([]);
async function initReviewCount() {
try {
@ -126,6 +133,14 @@
hasPermission.value = detail.errorCode !== 109001;
const { statusStatisticsMap, statusPercentList } = detail;
statusPercentValue.value = (statusPercentList || []).map((item, index) => {
return {
...item,
selected: true,
color: `${colorMapConfig[props.item.key][index]}`,
};
});
caseReviewCountOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
const { options: coverOptions, valueList } = handleUpdateTabPie(
statusStatisticsMap?.cover || [],

View File

@ -33,8 +33,13 @@
/>
</div>
</div>
<div class="h-[148px]">
<MsChart :options="countOptions" />
<div class="flex h-[148px]">
<LegendPieChart
v-model:currentPage="currentPage"
:has-permission="hasPermission"
:data="statusPercentValue"
:options="countOptions"
/>
</div>
</div>
</div>
@ -47,9 +52,9 @@
*/
import { ref } from 'vue';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import CardSkeleton from './cardSkeleton.vue';
import LegendPieChart, { legendDataType } from './legendPieChart.vue';
import PassRatePie from './passRatePie.vue';
import {
@ -69,7 +74,7 @@
} from '@/models/workbench/homePage';
import { WorkCardEnum } from '@/enums/workbenchEnum';
import { handlePieData, handleUpdateTabPie } from '../utils';
import { colorMapConfig, handlePieData, handleUpdateTabPie } from '../utils';
const appStore = useAppStore();
@ -120,6 +125,7 @@
const hasPermission = ref<boolean>(false);
const showSkeleton = ref(false);
const statusPercentValue = ref<legendDataType[]>([]);
async function initCount() {
try {
@ -140,6 +146,13 @@
const { statusStatisticsMap, statusPercentList, errorCode } = detail;
hasPermission.value = errorCode !== 109001;
statusPercentValue.value = (statusPercentList || []).map((item, index) => {
return {
...item,
selected: true,
color: `${colorMapConfig[props.item.key][index]}`,
};
});
countOptions.value = handlePieData(props.item.key, hasPermission.value, statusPercentList);
if (props.item.key === WorkCardEnum.PLAN_LEGACY_BUG) {
@ -183,6 +196,8 @@
});
}
const currentPage = ref(1);
onMounted(() => {
initCount();
});

View File

@ -140,7 +140,7 @@
})(countData),
};
});
options.value.yAxis[0].max = maxAxis < 100 ? 50 : maxAxis + 50;
options.value.yAxis[0].max = maxAxis <= 100 ? 100 : maxAxis + 50;
}
const showSkeleton = ref(false);

View File

@ -0,0 +1,172 @@
<template>
<div class="flex h-full w-full">
<div class="w-[180px]">
<MsChart ref="chartRef" :options="props.options" />
</div>
<div v-if="props.hasPermission" class="relative mt-[8px] h-full flex-1">
<!-- 图例部分 -->
<div class="flex w-full flex-col gap-4">
<div
v-for="(ele, i) of currentData"
:key="`ele.status-${i}`"
class="grid flex-1 grid-cols-3 gap-4"
@mouseover="handleMouseOver(ele.status)"
@mouseout="handleMouseOut(ele.status)"
>
<div class="flex items-center text-left text-[var(--color-text-3)]">
<div
:style="{
background: ele.selected ? `${ele.color}` : '#D4D4D8',
}"
class="mr-[8px] h-[8px] w-[8px] cursor-pointer rounded-lg"
@click="toggleLegend(ele.status, i)"
>
</div>
{{ ele.status }}
</div>
<div class="text-center">{{ ele.count }}</div>
<div class="text-right">{{ ele.percentValue }}</div>
</div>
</div>
<div v-if="totalPages > 1" class="legend-pagination">
<span :class="`toggle-button ${currentPage === 1 ? 'disabled' : ''}`" @click="prevPage">
<icon-caret-up
:class="`text-[14px] ${
currentPage === 1 ? 'text-[var(--color-text-brand)]' : 'text-[var(--color-text-4)]'
}`"
/>
</span>
<span class="mx-[4px] text-[var(--color-text-4)]">{{ currentPage }} / {{ totalPages }}</span>
<span :class="`toggle-button ${currentPage === totalPages ? 'disabled' : ''}`" @click="nextPage">
<icon-caret-down
:class="`text-[14px] ${
currentPage === totalPages ? 'text-[var(--color-text-brand)]' : 'text-[var(--color-text-4)]'
}`"
/>
</span>
</div>
</div>
<div v-else class="flex h-full flex-1 items-center justify-center">
<div class="rounded bg-[var(--color-text-n9)] px-[16px] py-[4px] text-[var(--color-text-4)]"
>{{ t('workbench.homePage.notHasResPermission') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import MsChart from '@/components/pure/chart/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
export interface legendDataType {
status: string;
count: number;
percentValue: string;
selected: boolean;
color: string;
}
const props = withDefaults(
defineProps<{
data: legendDataType[] | null;
itemsPerPage?: number; //
options: Record<string, any>;
hasPermission?: boolean;
}>(),
{
itemsPerPage: 4,
}
);
const currentPage = defineModel<number>('currentPage', { required: true });
const list = ref<legendDataType[]>([...(props.data || [])]);
//
const totalPages = computed(() => Math.ceil(list.value.length / props.itemsPerPage));
//
const currentData = computed(() => {
const startIndex = (currentPage.value - 1) * props.itemsPerPage;
const endIndex = startIndex + props.itemsPerPage;
return list.value.slice(startIndex, endIndex);
});
//
const prevPage = () => {
if (currentPage.value > 1) currentPage.value -= 1;
};
const nextPage = () => {
if (currentPage.value < totalPages.value) currentPage.value += 1;
};
//
const chartRef = ref<InstanceType<typeof MsChart>>();
function toggleLegend(name: string, index: number) {
const chart = chartRef.value?.chartRef;
if (chart) {
chart.dispatchAction({
type: 'legendToggleSelect',
name,
});
if (list.value) {
list.value[index].selected = !list.value[index].selected;
}
}
}
//
function handleMouseOver(name: string) {
const chart = chartRef.value?.chartRef;
if (chart) {
chart.dispatchAction({
type: 'highlight',
name,
});
}
}
//
function handleMouseOut(name: string) {
const chart = chartRef.value?.chartRef;
if (chart) {
chart.dispatchAction({
type: 'downplay',
name,
});
}
}
watch(
() => props.data,
(val) => {
list.value = [...(val || [])];
}
);
</script>
<style scoped>
.legend-pagination {
position: absolute;
right: 0;
bottom: -10px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
.toggle-button {
@apply cursor-pointer;
&.disabled {
cursor: not-allowed;
}
}
}
</style>

View File

@ -54,7 +54,6 @@
import { contentTabList } from '@/config/workbench';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { characterLimit } from '@/utils';
import type {
ModuleCardItem,
@ -128,7 +127,7 @@
options.value.graphic.invisible = invisible;
options.value.graphic.style.text = text;
// x
options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10));
options.value.xAxis.data = detail.xaxis;
const { maxAxis, data } = getSeriesData(detail.projectCountList);
options.value.series = data;

View File

@ -101,7 +101,7 @@
const { invisible, text } = handleNoDataDisplay(detail.xaxis, hasPermission.value);
options.value.graphic.invisible = invisible;
options.value.graphic.style.text = text;
options.value.xAxis.data = detail.xaxis.map((e) => characterLimit(e, 10));
options.value.xAxis.data = detail.xaxis;
const { maxAxis, data } = getSeriesData(detail.projectCountList);

View File

@ -32,8 +32,13 @@
/>
</div>
</div>
<div class="h-[148px]">
<MsChart :options="testPlanCountOptions" />
<div class="mt-[16px] h-[148px]">
<LegendPieChart
v-model:currentPage="currentPage"
:has-permission="hasPermission"
:data="statusPercentValue"
:options="testPlanCountOptions"
/>
</div>
</div>
</div>
@ -46,11 +51,10 @@
*/
import { ref } from 'vue';
import MsChart from '@/components/pure/chart/index.vue';
import MsSelect from '@/components/business/ms-select';
import CardSkeleton from './cardSkeleton.vue';
import LegendPieChart, { legendDataType } from './legendPieChart.vue';
import PassRatePie from './passRatePie.vue';
import TabCard from './tabCard.vue';
import { workTestPlanRage } from '@/api/modules/workbench';
import { useI18n } from '@/hooks/useI18n';
@ -63,7 +67,7 @@
WorkTestPlanRageDetail,
} from '@/models/workbench/homePage';
import { handlePieData, handleUpdateTabPie } from '../utils';
import { colorMapConfig, handlePieData, handleUpdateTabPie } from '../utils';
const { t } = useI18n();
const appStore = useAppStore();
@ -80,6 +84,7 @@
const innerProjectIds = defineModel<string[]>('projectIds', {
required: true,
});
const currentPage = ref(1);
const projectId = ref<string>(innerProjectIds.value[0]);
@ -122,6 +127,7 @@
//
const hasPermission = ref<boolean>(false);
const showSkeleton = ref(false);
const statusPercentValue = ref<legendDataType[]>([]);
async function initTestPlanCount() {
try {
@ -162,6 +168,14 @@
{ status: t('common.archived'), count: archived, percentValue: '0%' },
];
statusPercentValue.value = (statusPercentList || []).map((item, index) => {
return {
...item,
selected: true,
color: `${colorMapConfig[props.item.key][index]}`,
};
});
const total = statusPercentList.reduce((sum, item) => sum + item.count, 0);
const listStatusPercentList = statusPercentList.map((item) => ({

View File

@ -99,7 +99,8 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
},
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">
<div class="w-[186px] ms-scroll-bar max-h-[236px] overflow-y-auto p-[16px] gap-[8px] flex flex-col">
<div class="font-medium max-w-[150px] one-line-text" style="color:#323233">${params[0].axisValueLabel}</div>
${params
.map(
(item: any) => `
@ -136,6 +137,9 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
axisLabel: {
show: true,
color: '#646466',
width: 120,
overflow: 'truncate',
ellipsis: '...',
},
axisTick: {
show: false, // 隐藏刻度线
@ -274,7 +278,7 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[]): Record<s
}
// 下方饼图配置
export function getPieCharOptions(key: WorkCardEnum, hasPermission: boolean) {
export function getPieCharOptions(key: WorkCardEnum) {
return {
title: {
show: true,
@ -298,99 +302,8 @@ export function getPieCharOptions(key: WorkCardEnum, hasPermission: boolean) {
color: colorMapConfig[key],
tooltip: { show: true },
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,
show: false,
},
pageButtonPosition: 'end',
itemGap: 16,
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
bottom: 'center',
left: 180,
tooltip: {
show: false, // 禁用图例的 tooltip
},
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',
@ -413,20 +326,6 @@ export function getPieCharOptions(key: WorkCardEnum, hasPermission: boolean) {
},
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,
},
};
}
@ -463,7 +362,7 @@ export function handlePieData(
}[]
| null = []
) {
const options: Record<string, any> = getPieCharOptions(key, hasPermission);
const options: Record<string, any> = getPieCharOptions(key);
const lastStatusPercentList = statusPercentList ?? [];
options.series.data = lastStatusPercentList.map((item) => ({
name: item.status,
@ -490,13 +389,6 @@ export function handlePieData(
options.series.data = [];
}
// 设置图例的格式化函数,显示百分比
options.legend.formatter = (name: string) => {
return `{a|${tempObject[name].status}} {b|${addCommasToNumber(tempObject[name].count)}} {c|${
tempObject[name].percentValue
}}`;
};
return options;
}