379 lines
9.5 KiB
Vue
379 lines
9.5 KiB
Vue
<template>
|
||
<div class="index-module__NV_5cW__chartCard">
|
||
<h3 class="chartCard__title">基础设施覆盖率</h3>
|
||
<div class="chartCard__container">
|
||
<div class="num-empty" v-if="isEmpty"></div>
|
||
<div v-else id="infrastructureChart" class="chart"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { onMounted, ref, nextTick, watch, onUnmounted, computed } from 'vue';
|
||
import * as echarts from 'echarts';
|
||
import { usePageResize } from '@/utils/hooks/usePageResize';
|
||
import { infrastructurePage2 } from '@/api/jyh/situation';
|
||
|
||
// 基础颜色配置
|
||
const baseColors = ['#00F0FF', '#FFAB00', '#B20003'];
|
||
|
||
// 定义数据结构
|
||
interface InfrastructureItem {
|
||
name: string;
|
||
value: number;
|
||
}
|
||
|
||
// 响应式数据存储
|
||
const infrastructureData = ref<InfrastructureItem[]>([]);
|
||
const loading = ref(true);
|
||
|
||
// 从API获取数据
|
||
const fetchDataFromApi = async() => {
|
||
try {
|
||
loading.value = true;
|
||
const response = await infrastructurePage2();
|
||
const businessData = response.data; // Axios响应格式,业务数据在response.data中
|
||
|
||
if (businessData.code === 200 && businessData.data) {
|
||
// 为每个数据项添加颜色和半径配置
|
||
infrastructureData.value = businessData.data.map((item: InfrastructureItem, index: number) => ({
|
||
name: item.name,
|
||
value: item.value,
|
||
color: baseColors[index % baseColors.length],
|
||
radius: [`${70 - index * 20}%`, `${80 - index * 20}%`] // 根据索引计算半径,移除calc函数以避免潜在问题
|
||
}));
|
||
} else {
|
||
console.error('获取基础设施覆盖率数据失败:', businessData.msg || '未知错误');
|
||
// 设置默认数据
|
||
infrastructureData.value = [
|
||
{
|
||
name: '镜像站规模',
|
||
value: 85,
|
||
color: '#00F0FF',
|
||
radius: ['70%', '80%']
|
||
},
|
||
{
|
||
name: '代码托管平台规模',
|
||
value: 90,
|
||
color: '#FFAB00',
|
||
radius: ['50%', '60%']
|
||
},
|
||
{
|
||
name: '构建平台活跃度',
|
||
value: 40,
|
||
color: '#B20003',
|
||
radius: ['30%', '40%']
|
||
}
|
||
];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取基础设施覆盖率数据异常:', error);
|
||
// 设置默认数据
|
||
infrastructureData.value = [
|
||
{
|
||
name: '镜像站规模',
|
||
value: 85,
|
||
color: '#00F0FF',
|
||
radius: ['70%', '80%']
|
||
},
|
||
{
|
||
name: '代码托管平台规模',
|
||
value: 90,
|
||
color: '#FFAB00',
|
||
radius: ['50%', '60%']
|
||
},
|
||
{
|
||
name: '构建平台活跃度',
|
||
value: 40,
|
||
color: '#B20003',
|
||
radius: ['30%', '40%']
|
||
}
|
||
];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 组件挂载时获取数据
|
||
onMounted(() => {
|
||
fetchDataFromApi();
|
||
});
|
||
|
||
// 页面尺寸响应式
|
||
const { widthType } = usePageResize();
|
||
|
||
// 空数据标识
|
||
const isEmpty = computed(() => {
|
||
return infrastructureData.value.length === 0;
|
||
});
|
||
|
||
// 图表实例存储(用于resize时销毁重绘)
|
||
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
|
||
infrastructureChart: null
|
||
});
|
||
|
||
// 初始化基础设施覆盖率图表(多层环形图)
|
||
const initInfrastructureChart = () => {
|
||
const el = document.getElementById('infrastructureChart');
|
||
if (!el) return;
|
||
|
||
if (chartInstances.value.infrastructureChart) {
|
||
chartInstances.value.infrastructureChart.dispose();
|
||
}
|
||
|
||
const myChart = echarts.init(el);
|
||
chartInstances.value.infrastructureChart = myChart;
|
||
|
||
const seriesData: any[] = [];
|
||
const center = ['50%', '50%'];
|
||
|
||
infrastructureData.value.forEach(item => {
|
||
const completeValue = item.value;
|
||
const remainingValue = 100 - item.value;
|
||
|
||
seriesData.push({
|
||
name: item.name,
|
||
type: 'pie',
|
||
radius: item.radius,
|
||
center: center,
|
||
avoidLabelOverlap: false,
|
||
startAngle: 90,
|
||
hoverAnimation: true,
|
||
label: {
|
||
show: false
|
||
},
|
||
labelLine: {
|
||
show: false
|
||
},
|
||
emphasis: {
|
||
scale: true,
|
||
scaleSize: 5
|
||
},
|
||
data: [
|
||
{
|
||
value: completeValue,
|
||
name: item.name,
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(
|
||
0, 0, 0, 1,
|
||
[
|
||
{ offset: 0, color: item.color },
|
||
{ offset: 1, color: item.color + '80' }
|
||
]
|
||
),
|
||
shadowBlur: 10,
|
||
shadowColor: item.color + '40'
|
||
}
|
||
},
|
||
{
|
||
value: remainingValue,
|
||
name: '未覆盖',
|
||
itemStyle: {
|
||
color: 'rgba(255, 255, 255, 0.08)'
|
||
},
|
||
tooltip: {
|
||
show: false
|
||
},
|
||
emphasis: {
|
||
disabled: true
|
||
}
|
||
}
|
||
]
|
||
});
|
||
});
|
||
|
||
// 计算平均覆盖率
|
||
const avgCoverage = Math.round(
|
||
infrastructureData.value.reduce((sum, d) => sum + d.value, 0) /
|
||
infrastructureData.value.length
|
||
);
|
||
|
||
myChart.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||
borderColor: '#3b82f6',
|
||
borderWidth: 1,
|
||
borderRadius: 6,
|
||
padding: 12,
|
||
textStyle: { color: '#333', fontSize: 13 },
|
||
formatter: (params: any) => {
|
||
if (params.name === '未覆盖') return '';
|
||
const remaining = 100 - params.value;
|
||
return `
|
||
<div style="line-height: 2;">
|
||
<div style="font-weight: bold; color: #3b82f6; margin-bottom: 6px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${params.name}</div>
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span style="display: inline-block; width: 8px; height: 8px; background: #10b981; border-radius: 50%;"></span>
|
||
已覆盖:<span style="color: #10b981; font-weight: 700; font-size: 15px;">${params.value}%</span>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span style="display: inline-block; width: 8px; height: 8px; background: #ef4444; border-radius: 50%;"></span>
|
||
未覆盖:<span style="color: #ef4444; font-weight: 700; font-size: 15px;">${remaining}%</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
},
|
||
legend: {
|
||
data: infrastructureData.value.map(d => ({
|
||
name: d.name,
|
||
itemStyle: {
|
||
color: d.color
|
||
}
|
||
})),
|
||
orient: 'vertical',
|
||
right: '5%',
|
||
top: 'center',
|
||
itemWidth: 12, // 图例方块宽度
|
||
itemHeight: 12, // 图例方块高度
|
||
itemGap: 15, // 图例项之间的间距
|
||
icon: 'circle', // 图例形状:'circle'圆形, 'rect'方形, 'roundRect'圆角方形
|
||
textStyle: {
|
||
color: '#333',
|
||
fontSize: 12,
|
||
padding: [0, 0, 0, 8] // 文字与图标的间距
|
||
}
|
||
},
|
||
graphic: [
|
||
{
|
||
type: 'text',
|
||
left: 'center',
|
||
top: '45%',
|
||
style: {
|
||
text: '总覆盖度',
|
||
fill: '#666',
|
||
fontSize: 13,
|
||
fontWeight: '500'
|
||
}
|
||
},
|
||
{
|
||
type: 'text',
|
||
left: 'center',
|
||
top: '52%',
|
||
style: {
|
||
text: `${avgCoverage}%`,
|
||
fill: '#3b82f6',
|
||
fontSize: 28,
|
||
fontWeight: 'bold'
|
||
}
|
||
}
|
||
],
|
||
series: seriesData,
|
||
animationDuration: 1000,
|
||
animationEasing: 'cubicOut',
|
||
animationDelay: (idx: number) => idx * 100
|
||
});
|
||
};
|
||
|
||
// 监听数据变化,重新渲染图表
|
||
watch(infrastructureData, () => {
|
||
if (!isEmpty.value) {
|
||
nextTick(() => {
|
||
initInfrastructureChart();
|
||
});
|
||
}
|
||
}, { deep: true });
|
||
|
||
// 页面resize时重绘图表
|
||
watch(widthType, () => {
|
||
nextTick(() => {
|
||
if (!isEmpty.value) {
|
||
initInfrastructureChart();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 挂载时初始化
|
||
onMounted(() => {
|
||
setTimeout(() => {
|
||
if (!isEmpty.value) {
|
||
initInfrastructureChart();
|
||
}
|
||
}, 100);
|
||
});
|
||
|
||
// 组件卸载时销毁图表实例
|
||
onUnmounted(() => {
|
||
if (chartInstances.value.infrastructureChart) {
|
||
chartInstances.value.infrastructureChart.dispose();
|
||
chartInstances.value.infrastructureChart = null;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 基础设施覆盖率图表卡片样式 - 从主页面复制 */
|
||
.index-module__NV_5cW__chartCard {
|
||
-webkit-backdrop-filter: blur(10px);
|
||
backdrop-filter: blur(10px);
|
||
background: #ffffffe6;
|
||
border: 1px solid #ffffff4d;
|
||
border-radius: 1rem;
|
||
padding: 1.25rem;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.index-module__NV_5cW__chartCard::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
|
||
transition: left 0.6s;
|
||
}
|
||
|
||
.index-module__NV_5cW__chartCard:hover::before {
|
||
left: 100%;
|
||
}
|
||
|
||
.index-module__NV_5cW__chartCard:hover {
|
||
border-color: #3b82f64d;
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 24px #0000001f;
|
||
}
|
||
|
||
.index-module__NV_5cW__chartCard:hover .chartCard__title {
|
||
color: #3b82f6;
|
||
transform: scale(1.02);
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
/* 标题样式 - 从主页面复制 */
|
||
.chartCard__title {
|
||
color: #333;
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
margin: 0;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 容器样式 - 从主页面复制 */
|
||
.chartCard__container {
|
||
width: 100%;
|
||
height: 390px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 空数据显示样式 - 从主页面复制 */
|
||
.num-empty {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
|
||
/* 图表容器样式 */
|
||
.chart {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
</style> |