262 lines
7.6 KiB
Vue
262 lines
7.6 KiB
Vue
<template>
|
||
<div class="index-module__NV_5cW__chartCard mb-[1.5rem]">
|
||
<h3 class="chartCard__title">编程语言安全态势</h3>
|
||
<div class="chartCard__container">
|
||
<div class="num-empty" v-if="isEmpty.scatterData"></div>
|
||
<div v-else id="scatterChart" class="chart"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||
import * as echarts from 'echarts';
|
||
import { scatterDataPage1 } from '@/api/jyh/situation';
|
||
|
||
// 空数据标识
|
||
const isEmpty = ref({
|
||
scatterData: false
|
||
});
|
||
|
||
// 图表实例存储(用于resize时销毁重绘)
|
||
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
|
||
scatterChart: null
|
||
});
|
||
|
||
// 气泡图数据 - 响应式数据
|
||
const scatterData = ref({
|
||
scatterData: [] as any[],
|
||
midX: 50,
|
||
midY: 50,
|
||
quadrantColors: {
|
||
Q1: '#BF232A', // [高密度, 低活跃度] - 极高风险
|
||
Q2: '#FF4900', // [高密度, 高活跃度] - 高风险,需密切关注
|
||
Q3: '#00B43E', // [低密度, 低活跃度] - 低风险,但维护成本高
|
||
Q4: '#0063D0' // [低密度, 高活跃度] - 健康生态
|
||
}
|
||
});
|
||
|
||
// 获取后端数据
|
||
const fetchScatterData = async () => {
|
||
try {
|
||
console.log('开始请求编程语言安全态势数据...');
|
||
const response = await scatterDataPage1();
|
||
const businessData = response.data; // Axios响应格式,业务数据在response.data中
|
||
|
||
console.log('API响应:', response);
|
||
|
||
if (businessData?.code === 200) {
|
||
console.log('API返回的数据:', businessData.data);
|
||
|
||
// 处理后端返回的数据格式
|
||
const processedData = processBackendScatterData(businessData.data);
|
||
scatterData.value.scatterData = processedData;
|
||
|
||
// 更新空数据标识
|
||
isEmpty.value.scatterData = !processedData || processedData.length === 0;
|
||
} else {
|
||
console.error('获取编程语言安全态势数据失败:', businessData?.msg || '未知错误');
|
||
// 设置默认数据
|
||
setDefaultScatterData();
|
||
}
|
||
} catch (error) {
|
||
console.error('获取编程语言安全态势数据异常:', error);
|
||
// 发生异常时设置默认数据
|
||
setDefaultScatterData();
|
||
}
|
||
};
|
||
|
||
// 处理后端返回的数据格式
|
||
const processBackendScatterData = (backendData: any[]) => {
|
||
// 后端返回的数据已经是正确的格式,直接转换即可
|
||
return backendData.map(item => [
|
||
item.x, // 代码活跃度(X)
|
||
item.y, // 漏洞密度(Y)
|
||
item.size, // 漏洞总数(Size)
|
||
item.name // 语言名称(Name)
|
||
]);
|
||
};
|
||
|
||
// 设置默认数据
|
||
const setDefaultScatterData = () => {
|
||
scatterData.value.scatterData = [
|
||
[95, 75, 450, 'JavaScript'], // 最高活跃度:9,345,046个项目
|
||
[90, 70, 380, 'Python'], // 高活跃度:9,261,587个项目
|
||
[85, 65, 320, 'TypeScript'], // 高增长:78.1%增长率
|
||
[75, 80, 420, 'Java'], // 传统强势:3,520,215个项目
|
||
[45, 60, 280, 'C++'], // 中等活跃:1,701,552个项目
|
||
[40, 55, 250, 'C#'], // 中等活跃:1,478,463个项目
|
||
[35, 45, 180, 'PHP'], // 传统Web语言
|
||
[25, 25, 80, 'Go'], // 新兴语言,低漏洞密度
|
||
[15, 20, 45, 'Rust'], // 安全性较好的新语言
|
||
[60, 50, 200, 'Shell'], // 脚本语言
|
||
];
|
||
isEmpty.value.scatterData = false;
|
||
};
|
||
|
||
// 初始化气泡图
|
||
const initScatterChart = () => {
|
||
const el = document.getElementById('scatterChart');
|
||
if (!el) return;
|
||
|
||
// 延迟执行以确保DOM已渲染
|
||
setTimeout(() => {
|
||
const chartEl = document.getElementById('scatterChart');
|
||
if (!chartEl) return;
|
||
|
||
if (chartInstances.value.scatterChart) {
|
||
chartInstances.value.scatterChart.dispose();
|
||
}
|
||
|
||
const myChart = echarts.init(chartEl);
|
||
chartInstances.value.scatterChart = myChart;
|
||
|
||
// 数据处理
|
||
const data = scatterData.value.scatterData.map(item => ({
|
||
value: [item[0], item[1], item[2]],
|
||
name: item[3]
|
||
}));
|
||
|
||
// 计算四象限颜色
|
||
const getColor = (x: number, y: number) => {
|
||
if (x < scatterData.value.midX && y >= scatterData.value.midY) return scatterData.value.quadrantColors.Q1; // 高密度, 低活跃度 - 极高风险
|
||
if (x >= scatterData.value.midX && y >= scatterData.value.midY) return scatterData.value.quadrantColors.Q2; // 高密度, 高活跃度 - 高风险
|
||
if (x < scatterData.value.midX && y < scatterData.value.midY) return scatterData.value.quadrantColors.Q3; // 低密度, 低活跃度 - 低风险
|
||
return scatterData.value.quadrantColors.Q4; // 低密度, 高活跃度 - 健康生态
|
||
};
|
||
|
||
myChart.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: (params: any) => {
|
||
return `
|
||
<div>${params.data.name}</div>
|
||
<div>代码活跃度: ${params.data.value[0]}</div>
|
||
<div>漏洞密度: ${params.data.value[1]}</div>
|
||
<div>漏洞总数: ${params.data.value[2]}</div>
|
||
`;
|
||
}
|
||
},
|
||
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
|
||
xAxis: {
|
||
type: 'value',
|
||
name: '代码活跃度',
|
||
nameTextStyle: { fontSize: 12 },
|
||
axisLabel: { fontSize: 12 },
|
||
splitLine: { show: true }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
name: '漏洞密度',
|
||
nameTextStyle: { fontSize: 12 },
|
||
axisLabel: { fontSize: 12 },
|
||
splitLine: { show: true }
|
||
},
|
||
series: [{
|
||
type: 'scatter',
|
||
data: data,
|
||
symbolSize: (val: any) => val[2] / 2, // 根据漏洞总数调整气泡大小
|
||
itemStyle: {
|
||
color: (params: any) => getColor(params.value[0], params.value[1])
|
||
},
|
||
label: {
|
||
show: true,
|
||
formatter: (params: any) => params.data.name,
|
||
position: 'inside',
|
||
fontSize: 10,
|
||
color: '#fff'
|
||
}
|
||
}]
|
||
});
|
||
|
||
// 监听窗口大小变化,重新调整图表大小
|
||
window.addEventListener('resize', () => {
|
||
if (chartInstances.value.scatterChart) {
|
||
chartInstances.value.scatterChart.resize();
|
||
}
|
||
});
|
||
}, 0);
|
||
};
|
||
|
||
// 在组件挂载后获取数据并初始化图表
|
||
onMounted(async () => {
|
||
await fetchScatterData();
|
||
nextTick(() => {
|
||
initScatterChart();
|
||
});
|
||
});
|
||
|
||
// 监听数据变化,重新初始化图表
|
||
watch(scatterData, () => {
|
||
if (!isEmpty.value.scatterData) {
|
||
nextTick(() => {
|
||
initScatterChart();
|
||
});
|
||
}
|
||
}, { deep: true });
|
||
|
||
// 在组件卸载前清理资源
|
||
onUnmounted(() => {
|
||
if (chartInstances.value.scatterChart) {
|
||
chartInstances.value.scatterChart.dispose();
|
||
}
|
||
});
|
||
|
||
defineExpose({
|
||
initScatterChart
|
||
});
|
||
</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.5rem;
|
||
box-shadow: 0 4px 16px #00000014;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.index-module__NV_5cW__chartCard:hover {
|
||
border-color: #3b82f64d;
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 24px #0000001f;
|
||
}
|
||
|
||
.chartCard__title {
|
||
color: #333;
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
margin: 0 0 1rem 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.chartCard__container {
|
||
width: 100%;
|
||
height: 300px; /* 图表高度 */
|
||
position: relative;
|
||
}
|
||
|
||
.mb-\[1\.5rem\] {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
/* 与示例一致的空数据样式 */
|
||
.num-empty {
|
||
height: 10px;
|
||
border-top: 2px solid #2951E0;
|
||
width: 100%;
|
||
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.chart {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
</style> |