feat(工作台): 优化工作台所有柱状图
This commit is contained in:
parent
c71fd05a7c
commit
a51ea8702f
|
@ -1,9 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<VCharts v-if="renderChart" ref="chartRef" :option="options" :autoresize="autoResize" :style="{ width, height }" />
|
<VCharts v-if="chartId" ref="chartRef" :option="options" :autoresize="autoResize" :style="{ width, height }" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { getGenerateId } from '@/utils';
|
||||||
|
|
||||||
import { BarChart, CustomChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
|
import { BarChart, CustomChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
|
@ -54,11 +56,15 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderChart = ref(false);
|
|
||||||
|
|
||||||
const chartRef = ref<InstanceType<typeof VCharts>>();
|
const chartRef = ref<InstanceType<typeof VCharts>>();
|
||||||
nextTick(() => {
|
|
||||||
renderChart.value = true;
|
const chartId = ref('');
|
||||||
|
onMounted(() => {
|
||||||
|
chartId.value = getGenerateId();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
chartId.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
export default function bindDataZoomEvent(
|
||||||
|
chartRef: any,
|
||||||
|
options: Record<string, any>,
|
||||||
|
barWidth = 12,
|
||||||
|
barWidthMargin = 6,
|
||||||
|
barNumber = 7,
|
||||||
|
minBarGroupWidth = 24
|
||||||
|
) {
|
||||||
|
const chartDom = chartRef.value?.chartRef;
|
||||||
|
|
||||||
|
const handleDataZoom = (params: any) => {
|
||||||
|
const containerWidth = chartDom.getDom().offsetWidth;
|
||||||
|
// 计算缩放百分比
|
||||||
|
const percent = (params.end - params.start) / 100;
|
||||||
|
// 计算单组条形图的宽度(包括间隔和最小宽度)
|
||||||
|
const singleGroupWidth = barWidth * barNumber + barWidthMargin * (barNumber - 1) + minBarGroupWidth;
|
||||||
|
// 计算可视区域内的最大条形图组数
|
||||||
|
const maxVisibleGroups = Math.floor(containerWidth / singleGroupWidth);
|
||||||
|
// 根据缩放百分比,计算需要显示的分类数量
|
||||||
|
const val = options.value.xAxis.data.length * percent;
|
||||||
|
|
||||||
|
const calcCount = Math.ceil(val);
|
||||||
|
// 计算每个标签的宽度
|
||||||
|
const labelWidth = (containerWidth - calcCount * minBarGroupWidth) / calcCount;
|
||||||
|
// 更新图表的配置项,重新设置数据缩放和 x 轴标签的显示
|
||||||
|
chartDom.setOption(
|
||||||
|
{
|
||||||
|
...options.value,
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
...options.value.dataZoom[0],
|
||||||
|
start: params.start,
|
||||||
|
end: params.end,
|
||||||
|
maxValueSpan: maxVisibleGroups,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
axisLabel: {
|
||||||
|
width: labelWidth,
|
||||||
|
overflow: 'truncate',
|
||||||
|
ellipsis: '...',
|
||||||
|
interval: 0,
|
||||||
|
},
|
||||||
|
...options.value.xAxis,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ notMerge: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
chartDom.chart.on('dataZoom', handleDataZoom);
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (chartDom) {
|
||||||
|
const currentOptions = chartDom.chart.getOption();
|
||||||
|
if (currentOptions.dataZoom.length) {
|
||||||
|
handleDataZoom({ start: currentOptions.dataZoom[0].start, end: currentOptions.dataZoom[0].end });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
clear: () => {
|
||||||
|
chartDom.chart.off('dataZoom', handleDataZoom);
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
},
|
||||||
|
handleDataZoom,
|
||||||
|
};
|
||||||
|
}
|
|
@ -189,7 +189,7 @@ export const defaultValueMap: Record<string, any> = {
|
||||||
},
|
},
|
||||||
complete: {
|
complete: {
|
||||||
defaultList: cloneDeep(defaultComplete),
|
defaultList: cloneDeep(defaultComplete),
|
||||||
color: ['#00C261', '#3370FF', '#D4D4D8', '#FF9964'],
|
color: ['#D4D4D8', '#3370FF', '#00C261', '#FF9964'],
|
||||||
defaultName: 'workbench.homePage.completeRate',
|
defaultName: 'workbench.homePage.completeRate',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-[16px]">
|
<div class="mt-[16px]">
|
||||||
<MsChart height="260px" :options="options" />
|
<MsChart ref="chartRef" height="260px" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,17 +47,17 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import MsChart from '@/components/pure/chart/index.vue';
|
import MsChart from '@/components/pure/chart/index.vue';
|
||||||
|
import bindDataZoomEvent from '@/components/pure/chart/utils';
|
||||||
import MsSelect from '@/components/business/ms-select';
|
import MsSelect from '@/components/business/ms-select';
|
||||||
import CardSkeleton from './cardSkeleton.vue';
|
import CardSkeleton from './cardSkeleton.vue';
|
||||||
|
|
||||||
import { workBugHandlerDetail, workHandleUserOptions } from '@/api/modules/workbench';
|
import { workBugHandlerDetail, workHandleUserOptions } from '@/api/modules/workbench';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
import { characterLimit } from '@/utils';
|
|
||||||
|
|
||||||
import type { OverViewOfProject, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
import type { OverViewOfProject, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||||
|
|
||||||
import { getColorScheme, getCommonBarOptions, handleNoDataDisplay } from '../utils';
|
import { createCustomTooltip, getColorScheme, getSeriesData } from '../utils';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -97,50 +97,21 @@
|
||||||
const hasPermission = ref<boolean>(false);
|
const hasPermission = ref<boolean>(false);
|
||||||
|
|
||||||
function handleData(detail: OverViewOfProject) {
|
function handleData(detail: OverViewOfProject) {
|
||||||
options.value = getCommonBarOptions(detail.xaxis.length >= 7, [...defectStatusColor, ...getColorScheme(13)]);
|
const data = detail.projectCountList.map((e) => {
|
||||||
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));
|
|
||||||
|
|
||||||
let maxAxis = 5;
|
|
||||||
options.value.series = detail.projectCountList.map((item) => {
|
|
||||||
const countData: Record<string, any>[] = item.count.map((e) => {
|
|
||||||
return {
|
return {
|
||||||
name: item.name,
|
value: '',
|
||||||
value: e,
|
label: e.name,
|
||||||
originValue: e,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemMax = Math.max(...item.count);
|
options.value = getSeriesData(
|
||||||
|
data,
|
||||||
maxAxis = Math.max(itemMax, maxAxis);
|
detail,
|
||||||
return {
|
[...defectStatusColor, ...getColorScheme(13)],
|
||||||
name: item.name,
|
false,
|
||||||
type: 'bar',
|
true,
|
||||||
stack: 'bugMember',
|
props.item.fullScreen
|
||||||
barWidth: 12,
|
);
|
||||||
data: countData,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: [2, 2, 0, 0],
|
|
||||||
},
|
|
||||||
barMinHeight: ((optionData: Record<string, any>[]) => {
|
|
||||||
optionData.forEach((itemValue: any, index: number) => {
|
|
||||||
if (itemValue.value === 0) optionData[index].value = null;
|
|
||||||
});
|
|
||||||
let hasZero = false;
|
|
||||||
for (let i = 0; i < optionData.length; i++) {
|
|
||||||
if (optionData[i].value === 0) {
|
|
||||||
hasZero = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasZero ? 0 : 5;
|
|
||||||
})(countData),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
options.value.yAxis[0].max = maxAxis <= 100 ? 100 : maxAxis + 50;
|
|
||||||
}
|
}
|
||||||
const showSkeleton = ref(false);
|
const showSkeleton = ref(false);
|
||||||
|
|
||||||
|
@ -176,6 +147,7 @@
|
||||||
value: e.value,
|
value: e.value,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
const chartRef = ref<InstanceType<typeof MsChart>>();
|
||||||
|
|
||||||
async function handleProjectChange(isRefreshKey: boolean = false, setAll = false) {
|
async function handleProjectChange(isRefreshKey: boolean = false, setAll = false) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
@ -190,7 +162,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await nextTick();
|
await nextTick();
|
||||||
getDefectMemberDetail();
|
await getDefectMemberDetail();
|
||||||
|
const chartDom = chartRef.value?.chartRef;
|
||||||
|
|
||||||
|
if (chartDom && chartDom.chart) {
|
||||||
|
createCustomTooltip(chartDom);
|
||||||
|
bindDataZoomEvent(chartRef, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeProject() {
|
async function changeProject() {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="props.contentTabList.length" class="card-list">
|
||||||
|
<div v-for="ele of contentTabList" :key="ele.icon" class="card-list-item">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="card-title flex items-center gap-[8px]">
|
||||||
|
<div :class="`card-title-icon bg-[${ele?.color}]`">
|
||||||
|
<MsIcon :type="ele.icon" class="text-[var(--color-text-fff)]" size="12" />
|
||||||
|
</div>
|
||||||
|
<div class="text-[var(--color-text-1)]"> {{ ele.label }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-number !text-[20px] !font-medium"> {{ addCommasToNumber(ele.count || 0) }} </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NoData v-else :no-permission-text="props.noPermissionText" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import NoData from '../../components/notData.vue';
|
||||||
|
|
||||||
|
import { addCommasToNumber } from '@/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
contentTabList: {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
count?: number;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}[];
|
||||||
|
noPermissionText?: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.card-list {
|
||||||
|
gap: 16px;
|
||||||
|
@apply flex flex-1;
|
||||||
|
.card-list-item {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--color-text-n8);
|
||||||
|
border-radius: 4px;
|
||||||
|
@apply flex-1;
|
||||||
|
.card-title-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
.card-number {
|
||||||
|
margin-left: 28px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -30,7 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-[16px]">
|
<div class="my-[16px]">
|
||||||
<TabCard
|
<HeaderCard
|
||||||
:content-tab-list="cardModuleList"
|
:content-tab-list="cardModuleList"
|
||||||
:no-permission-text="hasPermission ? '' : 'workbench.homePage.notHasResPermission'"
|
:no-permission-text="hasPermission ? '' : 'workbench.homePage.notHasResPermission'"
|
||||||
/>
|
/>
|
||||||
|
@ -50,9 +50,10 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import MsChart from '@/components/pure/chart/index.vue';
|
import MsChart from '@/components/pure/chart/index.vue';
|
||||||
|
import bindDataZoomEvent from '@/components/pure/chart/utils';
|
||||||
import MsSelect from '@/components/business/ms-select';
|
import MsSelect from '@/components/business/ms-select';
|
||||||
import CardSkeleton from './cardSkeleton.vue';
|
import CardSkeleton from './cardSkeleton.vue';
|
||||||
import TabCard from './tabCard.vue';
|
import HeaderCard from './headerCard.vue';
|
||||||
|
|
||||||
import { workMyCreatedDetail, workProOverviewDetail } from '@/api/modules/workbench';
|
import { workMyCreatedDetail, workProOverviewDetail } from '@/api/modules/workbench';
|
||||||
import { contentTabList } from '@/config/workbench';
|
import { contentTabList } from '@/config/workbench';
|
||||||
|
@ -191,12 +192,21 @@
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initOverViewDetail();
|
await initOverViewDetail();
|
||||||
|
|
||||||
setTimeout(() => {
|
nextTick(() => {
|
||||||
const chartDom = chartRef.value?.chartRef;
|
const chartDom = chartRef.value?.chartRef;
|
||||||
|
|
||||||
if (chartDom && chartDom.chart) {
|
if (chartDom && chartDom.chart) {
|
||||||
createCustomTooltip(chartDom);
|
createCustomTooltip(chartDom);
|
||||||
|
bindDataZoomEvent(chartRef, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const unbindDataZoom = bindDataZoomEvent(chartRef, options);
|
||||||
|
if (unbindDataZoom) {
|
||||||
|
unbindDataZoom.clear();
|
||||||
}
|
}
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import MsChart from '@/components/pure/chart/index.vue';
|
import MsChart from '@/components/pure/chart/index.vue';
|
||||||
|
import bindDataZoomEvent from '@/components/pure/chart/utils';
|
||||||
import MsSelect from '@/components/business/ms-select';
|
import MsSelect from '@/components/business/ms-select';
|
||||||
import CardSkeleton from './cardSkeleton.vue';
|
import CardSkeleton from './cardSkeleton.vue';
|
||||||
|
|
||||||
|
@ -149,12 +150,12 @@
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await initOverViewMemberDetail();
|
await initOverViewMemberDetail();
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const chartDom = chartRef.value?.chartRef;
|
const chartDom = chartRef.value?.chartRef;
|
||||||
|
|
||||||
if (chartDom && chartDom.chart) {
|
if (chartDom && chartDom.chart) {
|
||||||
createCustomTooltip(chartDom);
|
createCustomTooltip(chartDom);
|
||||||
|
bindDataZoomEvent(chartRef, options);
|
||||||
}
|
}
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeProject() {
|
async function changeProject() {
|
||||||
|
@ -214,6 +215,13 @@
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleProjectChange(false);
|
handleProjectChange(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const unbindDataZoom = bindDataZoomEvent(chartRef, options);
|
||||||
|
if (unbindDataZoom) {
|
||||||
|
unbindDataZoom.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less"></style>
|
<style scoped lang="less"></style>
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
<template>
|
|
||||||
<div ref="cardWrapperRef">
|
|
||||||
<a-tabs v-if="props.contentTabList.length" default-active-key="1" class="ms-tab-card">
|
|
||||||
<a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="`${item.label}`">
|
|
||||||
<template #title>
|
|
||||||
<slot name="item" :item="item">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="card-title flex items-center gap-[8px]">
|
|
||||||
<div :class="`card-title-icon bg-[${item?.color}]`">
|
|
||||||
<MsIcon :type="item.icon" class="text-white" size="12" />
|
|
||||||
</div>
|
|
||||||
<div class="text-[var(--color-text-1)]"> {{ item.label }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-number !text-[20px] !font-medium"> {{ addCommasToNumber(item.count || 0) }} </div>
|
|
||||||
</div>
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
|
||||||
<NoData v-else :no-permission-text="props.noPermissionText" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
|
|
||||||
import NoData from '../../components/notData.vue';
|
|
||||||
|
|
||||||
import { addCommasToNumber } from '@/utils';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
contentTabList: {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
count?: number;
|
|
||||||
icon?: string;
|
|
||||||
color?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}[];
|
|
||||||
minWidth?: string;
|
|
||||||
notHasPadding?: boolean;
|
|
||||||
hiddenBorder?: boolean;
|
|
||||||
noPermissionText?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const width = ref<string | number>();
|
|
||||||
|
|
||||||
const cardWrapperRef = ref<HTMLElement | null>(null);
|
|
||||||
const calculateWidth = debounce(() => {
|
|
||||||
const wrapperContent = cardWrapperRef.value as HTMLElement;
|
|
||||||
if (wrapperContent) {
|
|
||||||
const wrapperTotalWidth = wrapperContent.offsetWidth;
|
|
||||||
const gap = 16;
|
|
||||||
const gapWidth = (props.contentTabList.length - 1) * gap; // 总间隙宽度
|
|
||||||
const itemWidth = Math.floor((wrapperTotalWidth - gapWidth) / props.contentTabList.length);
|
|
||||||
width.value = `${itemWidth}px`;
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const wrapperContent = cardWrapperRef.value;
|
|
||||||
if (wrapperContent) {
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
calculateWidth();
|
|
||||||
});
|
|
||||||
resizeObserver.observe(wrapperContent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const minwidth = ref();
|
|
||||||
const padding = ref();
|
|
||||||
const color = ref();
|
|
||||||
watch(
|
|
||||||
[() => props.minWidth, () => props.notHasPadding, () => props.hiddenBorder],
|
|
||||||
(val) => {
|
|
||||||
const [newMinWidth, noPadding, isHiddenBorder] = val;
|
|
||||||
minwidth.value = `${newMinWidth || '136px'}`;
|
|
||||||
padding.value = `${noPadding ? '0px' : '16px'}`;
|
|
||||||
color.value = `${isHiddenBorder ? 'transparent' : 'var(--color-text-n8)'}`;
|
|
||||||
calculateWidth();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.contentTabList,
|
|
||||||
(val) => {
|
|
||||||
if (val.length) {
|
|
||||||
calculateWidth();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('resize', calculateWidth);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
:deep(.ms-tab-card) {
|
|
||||||
.arco-tabs-nav-tab {
|
|
||||||
.arco-tabs-nav-ink {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.arco-tabs-nav-tab-list {
|
|
||||||
gap: 16px;
|
|
||||||
@apply flex;
|
|
||||||
}
|
|
||||||
.arco-tabs-tab {
|
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: v-bind(padding) !important;
|
|
||||||
width: v-bind(width);
|
|
||||||
min-width: v-bind(minwidth) !important;
|
|
||||||
border: 1px solid v-bind(color);
|
|
||||||
border-radius: 4px;
|
|
||||||
&.arco-tabs-tab-active {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-tabs-tab-title {
|
|
||||||
@apply w-full;
|
|
||||||
.card-number {
|
|
||||||
margin-left: 28px;
|
|
||||||
font-size: 20px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-title-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
@apply flex items-center justify-center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.arco-tabs-nav-button) {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border: 1px solid var(--color-text-n8);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -191,16 +191,16 @@
|
||||||
count: completeRate,
|
count: completeRate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('common.completed'),
|
name: t('common.notStarted'),
|
||||||
count: finished,
|
count: prepared,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('common.inProgress'),
|
name: t('common.inProgress'),
|
||||||
count: running,
|
count: running,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('common.notStarted'),
|
name: t('common.completed'),
|
||||||
count: prepared,
|
count: finished,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('common.archived'),
|
name: t('common.archived'),
|
||||||
|
|
|
@ -71,19 +71,7 @@
|
||||||
<MsChart height="76px" width="76px" :options="execOptions" />
|
<MsChart height="76px" width="76px" :options="execOptions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-list">
|
<HeaderCard :content-tab-list="cardModuleList" />
|
||||||
<div v-for="ele of cardModuleList" :key="ele.icon" class="card-list-item">
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="card-title flex items-center gap-[8px]">
|
|
||||||
<div :class="`card-title-icon bg-[${ele?.color}]`">
|
|
||||||
<MsIcon :type="ele.icon" class="text-white" size="12" />
|
|
||||||
</div>
|
|
||||||
<div class="text-[var(--color-text-1)]"> {{ ele.label }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-number !text-[20px] !font-medium"> {{ addCommasToNumber(ele.count || 0) }} </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<MsChart ref="chartRef" height="280px" :options="options" />
|
<MsChart ref="chartRef" height="280px" :options="options" />
|
||||||
|
@ -100,16 +88,18 @@
|
||||||
import { CascaderOption } from '@arco-design/web-vue';
|
import { CascaderOption } from '@arco-design/web-vue';
|
||||||
|
|
||||||
import MsChart from '@/components/pure/chart/index.vue';
|
import MsChart from '@/components/pure/chart/index.vue';
|
||||||
|
import bindDataZoomEvent from '@/components/pure/chart/utils';
|
||||||
import MsCascader from '@/components/business/ms-cascader/index.vue';
|
import MsCascader from '@/components/business/ms-cascader/index.vue';
|
||||||
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
|
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
|
||||||
import CardSkeleton from './cardSkeleton.vue';
|
import CardSkeleton from './cardSkeleton.vue';
|
||||||
|
import HeaderCard from './headerCard.vue';
|
||||||
import ThresholdProgress from './thresholdProgress.vue';
|
import ThresholdProgress from './thresholdProgress.vue';
|
||||||
|
|
||||||
import { getWorkTestPlanListUrl, workTestPlanOverviewDetail } from '@/api/modules/workbench';
|
import { getWorkTestPlanListUrl, workTestPlanOverviewDetail } from '@/api/modules/workbench';
|
||||||
import { commonRatePieOptions } from '@/config/workbench';
|
import { commonRatePieOptions } from '@/config/workbench';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
import { addCommasToNumber, findNodePathByKey, mapTree } from '@/utils';
|
import { findNodePathByKey, mapTree } from '@/utils';
|
||||||
|
|
||||||
import type { ModuleCardItem, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
import type { ModuleCardItem, SelectedCardItem, TimeFormParams } from '@/models/workbench/homePage';
|
||||||
import { TestPlanStatusEnum } from '@/enums/testPlanEnum';
|
import { TestPlanStatusEnum } from '@/enums/testPlanEnum';
|
||||||
|
@ -388,12 +378,11 @@
|
||||||
|
|
||||||
await initOverViewDetail();
|
await initOverViewDetail();
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const chartDom = chartRef.value?.chartRef;
|
const chartDom = chartRef.value?.chartRef;
|
||||||
if (chartDom && chartDom.chart) {
|
if (chartDom && chartDom.chart) {
|
||||||
createCustomTooltip(chartDom);
|
createCustomTooltip(chartDom);
|
||||||
|
bindDataZoomEvent(chartRef, options);
|
||||||
}
|
}
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRefreshKeyChange() {
|
async function handleRefreshKeyChange() {
|
||||||
|
@ -455,7 +444,7 @@
|
||||||
.threshold-card-item {
|
.threshold-card-item {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
width: 34%;
|
width: 34%;
|
||||||
height: 76px;
|
height: 78px;
|
||||||
border: 1px solid var(--color-text-n8);
|
border: 1px solid var(--color-text-n8);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
|
@ -71,7 +71,7 @@ export const colorMapConfig: Record<string, string[]> = {
|
||||||
[WorkCardEnum.CASE_COUNT]: ['#ED0303', '#FFA200', '#3370FF', '#D4D4D8'],
|
[WorkCardEnum.CASE_COUNT]: ['#ED0303', '#FFA200', '#3370FF', '#D4D4D8'],
|
||||||
[WorkCardEnum.ASSOCIATE_CASE_COUNT]: ['#00C261', '#3370FF'],
|
[WorkCardEnum.ASSOCIATE_CASE_COUNT]: ['#00C261', '#3370FF'],
|
||||||
[WorkCardEnum.REVIEW_CASE_COUNT]: ['#D4D4D8', '#3370FF', '#00C261', '#ED0303', '#FFA200'],
|
[WorkCardEnum.REVIEW_CASE_COUNT]: ['#D4D4D8', '#3370FF', '#00C261', '#ED0303', '#FFA200'],
|
||||||
[WorkCardEnum.TEST_PLAN_COUNT]: ['#9441B1', '#3370FF', '#00C261', '#D4D4D8'],
|
[WorkCardEnum.TEST_PLAN_COUNT]: ['#D4D4D8', '#3370FF', '#00C261', '#FF9964'],
|
||||||
[WorkCardEnum.PLAN_LEGACY_BUG]: ['#FFA200', '#3370FF', '#D4D4D8', '#00C261', ...getColorScheme(13)],
|
[WorkCardEnum.PLAN_LEGACY_BUG]: ['#FFA200', '#3370FF', '#D4D4D8', '#00C261', ...getColorScheme(13)],
|
||||||
[WorkCardEnum.BUG_COUNT]: ['#FFA200', '#3370FF', '#D4D4D8', '#00C261', ...getColorScheme(13)],
|
[WorkCardEnum.BUG_COUNT]: ['#FFA200', '#3370FF', '#D4D4D8', '#00C261', ...getColorScheme(13)],
|
||||||
[WorkCardEnum.HANDLE_BUG_BY_ME]: ['#FFA200', '#3370FF', '#D4D4D8', '#00C261', ...getColorScheme(13)],
|
[WorkCardEnum.HANDLE_BUG_BY_ME]: ['#FFA200', '#3370FF', '#D4D4D8', '#00C261', ...getColorScheme(13)],
|
||||||
|
@ -81,7 +81,12 @@ export const colorMapConfig: Record<string, string[]> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 柱状图
|
// 柱状图
|
||||||
export function getCommonBarOptions(hasRoom: boolean, color: string[], isTestPlan = false): Record<string, any> {
|
export function getCommonBarOptions(
|
||||||
|
hasRoom: boolean,
|
||||||
|
color: string[],
|
||||||
|
isTestPlan = false,
|
||||||
|
fullScreen = true
|
||||||
|
): Record<string, any> {
|
||||||
return {
|
return {
|
||||||
tooltip: [
|
tooltip: [
|
||||||
{
|
{
|
||||||
|
@ -158,12 +163,11 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[], isTestPla
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
show: true,
|
show: true,
|
||||||
color: '#646466',
|
color: '#646466',
|
||||||
width: 120,
|
width: 100,
|
||||||
overflow: 'truncate',
|
overflow: 'truncate',
|
||||||
ellipsis: '...',
|
ellipsis: '...',
|
||||||
showMinLabel: true,
|
showMinLabel: true,
|
||||||
showMaxLabel: true,
|
showMaxLabel: true,
|
||||||
// TOTO 等待优化
|
|
||||||
interval: 0,
|
interval: 0,
|
||||||
},
|
},
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
|
@ -186,7 +190,6 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[], isTestPla
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
alignTicks: true,
|
alignTicks: true,
|
||||||
name: t('workbench.homePage.unit'), // 设置单位
|
|
||||||
position: 'left',
|
position: 'left',
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: false,
|
show: false,
|
||||||
|
@ -272,12 +275,13 @@ export function getCommonBarOptions(hasRoom: boolean, color: string[], isTestPla
|
||||||
type: 'slider',
|
type: 'slider',
|
||||||
height: 24,
|
height: 24,
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
// TODO 待优化
|
realtime: true,
|
||||||
minSpan: 1,
|
minSpan: 1,
|
||||||
maxSpan: 26,
|
maxValueSpan: fullScreen ? 12 : 6,
|
||||||
startValue: 0,
|
startValue: 0,
|
||||||
end: 30,
|
end: 30,
|
||||||
rangeMode: ['value', 'percent'], // 起点按实际值,终点按百分比动态计算
|
endValue: fullScreen ? 12 : 6,
|
||||||
|
rangeMode: ['percent', 'percent'], // 起点按实际值,终点按百分比动态计算
|
||||||
showDataShadow: 'auto',
|
showDataShadow: 'auto',
|
||||||
showDetail: false,
|
showDetail: false,
|
||||||
filterMode: 'none',
|
filterMode: 'none',
|
||||||
|
@ -538,9 +542,9 @@ export const routeNavigationMap: Record<string, any> = {
|
||||||
},
|
},
|
||||||
complete: {
|
complete: {
|
||||||
status: [
|
status: [
|
||||||
WorkNavValueEnum.TEST_PLAN_COMPLETED, // 测试计划-已完成
|
|
||||||
WorkNavValueEnum.TEST_PLAN_UNDERWAY, // 测试计划-进行中
|
|
||||||
WorkNavValueEnum.TEST_PLAN_PREPARED, // 测试计划-未开始
|
WorkNavValueEnum.TEST_PLAN_PREPARED, // 测试计划-未开始
|
||||||
|
WorkNavValueEnum.TEST_PLAN_UNDERWAY, // 测试计划-进行中
|
||||||
|
WorkNavValueEnum.TEST_PLAN_COMPLETED, // 测试计划-已完成
|
||||||
WorkNavValueEnum.TEST_PLAN_ARCHIVED, // 测试计划-已归档
|
WorkNavValueEnum.TEST_PLAN_ARCHIVED, // 测试计划-已归档
|
||||||
],
|
],
|
||||||
route: RouteEnum.TEST_PLAN_INDEX,
|
route: RouteEnum.TEST_PLAN_INDEX,
|
||||||
|
@ -637,14 +641,16 @@ export function getSeriesData(
|
||||||
contentTabList: ModuleCardItem[],
|
contentTabList: ModuleCardItem[],
|
||||||
detail: OverViewOfProject,
|
detail: OverViewOfProject,
|
||||||
colorConfig: string[],
|
colorConfig: string[],
|
||||||
isTestPlan = false
|
isTestPlan = false,
|
||||||
|
isStack = false,
|
||||||
|
fullScreen = true
|
||||||
) {
|
) {
|
||||||
let options: Record<string, any> = {};
|
let options: Record<string, any> = {};
|
||||||
|
|
||||||
const { projectCountList, xaxis, errorCode } = detail;
|
const { projectCountList, xaxis, errorCode } = detail;
|
||||||
const hasPermission = errorCode !== 109001;
|
const hasPermission = errorCode !== 109001;
|
||||||
|
|
||||||
options = getCommonBarOptions(xaxis.length >= 7, colorConfig, isTestPlan);
|
options = getCommonBarOptions(xaxis.length >= 7, colorConfig, isTestPlan, fullScreen);
|
||||||
options.xAxis.data = xaxis;
|
options.xAxis.data = xaxis;
|
||||||
const { invisible, text } = handleNoDataDisplay(xaxis, hasPermission);
|
const { invisible, text } = handleNoDataDisplay(xaxis, hasPermission);
|
||||||
options.graphic.invisible = invisible;
|
options.graphic.invisible = invisible;
|
||||||
|
@ -654,7 +660,7 @@ export function getSeriesData(
|
||||||
const seriesData = projectCountList.map((item, sid) => {
|
const seriesData = projectCountList.map((item, sid) => {
|
||||||
const countData: Record<string, any>[] = item.count.map((e) => {
|
const countData: Record<string, any>[] = item.count.map((e) => {
|
||||||
return {
|
return {
|
||||||
name: t(contentTabList[sid].label),
|
name: t(contentTabList[sid]?.label ?? ''),
|
||||||
value: e,
|
value: e,
|
||||||
originValue: e,
|
originValue: e,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
@ -683,8 +689,8 @@ export function getSeriesData(
|
||||||
|
|
||||||
maxAxis = Math.max(itemMax, maxAxis);
|
maxAxis = Math.max(itemMax, maxAxis);
|
||||||
|
|
||||||
return {
|
const itemSeries: Record<string, any> = {
|
||||||
name: t(contentTabList[sid].label),
|
name: t(contentTabList[sid]?.label ?? ''),
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
barWidth: 12,
|
barWidth: 12,
|
||||||
legendHoverLink: true,
|
legendHoverLink: true,
|
||||||
|
@ -692,7 +698,6 @@ export function getSeriesData(
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: [2, 2, 0, 0],
|
borderRadius: [2, 2, 0, 0],
|
||||||
},
|
},
|
||||||
barCategoryGap: 24,
|
|
||||||
data: countData,
|
data: countData,
|
||||||
barMinHeight: ((optionData: Record<string, any>[]) => {
|
barMinHeight: ((optionData: Record<string, any>[]) => {
|
||||||
optionData.forEach((itemValue: any, index: number) => {
|
optionData.forEach((itemValue: any, index: number) => {
|
||||||
|
@ -708,6 +713,12 @@ export function getSeriesData(
|
||||||
return hasZero ? 0 : 5;
|
return hasZero ? 0 : 5;
|
||||||
})(countData),
|
})(countData),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isStack) {
|
||||||
|
itemSeries.stack = 'stack';
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemSeries;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 动态步长调整函数
|
// 动态步长调整函数
|
||||||
|
@ -752,7 +763,7 @@ export function createCustomTooltip(chartDom: InstanceType<typeof VCharts>) {
|
||||||
customTooltip.textContent = `${params.value}`;
|
customTooltip.textContent = `${params.value}`;
|
||||||
customTooltip.style.display = 'block';
|
customTooltip.style.display = 'block';
|
||||||
|
|
||||||
customTooltip.style.left = `${clientX - 20}px`;
|
customTooltip.style.left = `${clientX}px`;
|
||||||
customTooltip.style.top = `${clientY + 10}px`;
|
customTooltip.style.top = `${clientY + 10}px`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue