2026-01-26 15:40:52 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="index-module__NV_5cW__chartCard">
|
|
|
|
|
|
<h3 class="chartCard__title">高危组件 Top 10</h3>
|
|
|
|
|
|
<div class="chartCard__container">
|
|
|
|
|
|
<div class="num-empty" v-if="isEmpty"></div>
|
|
|
|
|
|
<div v-else ref="chartRef" class="chart"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, ref, nextTick, watch, onUnmounted } from 'vue';
|
|
|
|
|
|
import * as echarts from 'echarts';
|
2026-01-27 10:50:43 +08:00
|
|
|
|
import { highRiskComponentPage1 } from '@/api/jyh/situation';
|
|
|
|
|
|
|
|
|
|
|
|
// 定义组件数据结构
|
|
|
|
|
|
interface ComponentData {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
cvss: number;
|
|
|
|
|
|
vulnCount: number;
|
|
|
|
|
|
usage: string;
|
|
|
|
|
|
desc: string;
|
|
|
|
|
|
}
|
2026-01-26 15:40:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 定义属性,允许从父组件传入数据
|
|
|
|
|
|
const props = defineProps<{
|
2026-01-27 10:50:43 +08:00
|
|
|
|
data?: Array<ComponentData>;
|
2026-01-26 15:40:52 +08:00
|
|
|
|
loading?: boolean;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
// 定义 emits 事件
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: 'chart-ready', chart: echarts.ECharts): void;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
// 空数据标识
|
|
|
|
|
|
const isEmpty = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 图表DOM引用
|
|
|
|
|
|
const chartRef = ref<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 图表实例存储
|
|
|
|
|
|
let chartInstance: echarts.ECharts | null = null;
|
|
|
|
|
|
|
2026-01-27 10:50:43 +08:00
|
|
|
|
// 组件数据
|
|
|
|
|
|
const componentData = ref<ComponentData[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取高危组件数据
|
|
|
|
|
|
const fetchComponentData = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await highRiskComponentPage1();
|
2026-01-30 10:22:54 +08:00
|
|
|
|
if (response.data.code === 200) {
|
|
|
|
|
|
componentData.value = response.data.data;
|
2026-01-27 10:50:43 +08:00
|
|
|
|
} else {
|
2026-01-30 10:22:54 +08:00
|
|
|
|
console.error('获取高危组件数据失败:', response.data.msg || '未知错误');
|
2026-01-27 10:50:43 +08:00
|
|
|
|
// 如果接口调用失败,使用默认数据
|
|
|
|
|
|
componentData.value = [
|
|
|
|
|
|
{ name: 'Log4j2', cvss: 9.8, vulnCount: 15, usage: '1200万+', desc: 'CVE-2021-44228等严重RCE漏洞' },
|
|
|
|
|
|
{ name: 'Apache Struts2', cvss: 9.7, vulnCount: 12, usage: '850万+', desc: 'S2系列远程代码执行漏洞' },
|
|
|
|
|
|
{ name: 'OpenSSL', cvss: 9.6, vulnCount: 18, usage: '2500万+', desc: '心脏出血、幽灵猫等高危漏洞' },
|
|
|
|
|
|
{ name: 'Spring Framework', cvss: 9.5, vulnCount: 9, usage: '1800万+', desc: 'Spring4Shell等RCE漏洞' },
|
|
|
|
|
|
{ name: 'Jackson', cvss: 9.4, vulnCount: 11, usage: '1000万+', desc: '反序列化漏洞,影响Java生态' },
|
|
|
|
|
|
{ name: 'Fastjson', cvss: 9.3, vulnCount: 14, usage: '600万+', desc: '多个反序列化RCE漏洞' },
|
|
|
|
|
|
{ name: 'Apache Commons', cvss: 9.2, vulnCount: 8, usage: '1500万+', desc: 'Collections反序列化漏洞' },
|
|
|
|
|
|
{ name: 'jQuery', cvss: 9.1, vulnCount: 6, usage: '3000万+', desc: 'XSS和原型污染漏洞' },
|
|
|
|
|
|
{ name: 'Lodash', cvss: 9.0, vulnCount: 5, usage: '2200万+', desc: '原型污染漏洞,影响Node.js生态' },
|
|
|
|
|
|
{ name: 'Moment.js', cvss: 9.0, vulnCount: 4, usage: '1100万+', desc: 'ReDoS和原型污染漏洞' }
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('请求高危组件数据时发生错误:', error);
|
|
|
|
|
|
// 发生异常时使用默认数据
|
|
|
|
|
|
componentData.value = [
|
|
|
|
|
|
{ name: 'Log4j2', cvss: 9.8, vulnCount: 15, usage: '1200万+', desc: 'CVE-2021-44228等严重RCE漏洞' },
|
|
|
|
|
|
{ name: 'Apache Struts2', cvss: 9.7, vulnCount: 12, usage: '850万+', desc: 'S2系列远程代码执行漏洞' },
|
|
|
|
|
|
{ name: 'OpenSSL', cvss: 9.6, vulnCount: 18, usage: '2500万+', desc: '心脏出血、幽灵猫等高危漏洞' },
|
|
|
|
|
|
{ name: 'Spring Framework', cvss: 9.5, vulnCount: 9, usage: '1800万+', desc: 'Spring4Shell等RCE漏洞' },
|
|
|
|
|
|
{ name: 'Jackson', cvss: 9.4, vulnCount: 11, usage: '1000万+', desc: '反序列化漏洞,影响Java生态' },
|
|
|
|
|
|
{ name: 'Fastjson', cvss: 9.3, vulnCount: 14, usage: '600万+', desc: '多个反序列化RCE漏洞' },
|
|
|
|
|
|
{ name: 'Apache Commons', cvss: 9.2, vulnCount: 8, usage: '1500万+', desc: 'Collections反序列化漏洞' },
|
|
|
|
|
|
{ name: 'jQuery', cvss: 9.1, vulnCount: 6, usage: '3000万+', desc: 'XSS和原型污染漏洞' },
|
|
|
|
|
|
{ name: 'Lodash', cvss: 9.0, vulnCount: 5, usage: '2200万+', desc: '原型污染漏洞,影响Node.js生态' },
|
|
|
|
|
|
{ name: 'Moment.js', cvss: 9.0, vulnCount: 4, usage: '1100万+', desc: 'ReDoS和原型污染漏洞' }
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-26 15:40:52 +08:00
|
|
|
|
// 初始化图表
|
|
|
|
|
|
const initChart = async () => {
|
|
|
|
|
|
if (!chartRef.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 销毁之前的实例
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
|
chartInstance.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
chartInstance = echarts.init(chartRef.value);
|
|
|
|
|
|
|
2026-01-27 10:50:43 +08:00
|
|
|
|
// 如果有传入的数据,则优先使用传入的数据,否则使用获取到的数据
|
|
|
|
|
|
const chartData = props.data || componentData.value;
|
2026-01-26 15:40:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否有有效数据
|
|
|
|
|
|
isEmpty.value = !chartData || chartData.length === 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (isEmpty.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 准备图表数据
|
|
|
|
|
|
const componentNames = chartData.map(item => item.name);
|
|
|
|
|
|
const cvssScores = chartData.map(item => item.cvss);
|
|
|
|
|
|
const vulnCounts = chartData.map(item => item.vulnCount);
|
|
|
|
|
|
const usages = chartData.map(item => item.usage);
|
|
|
|
|
|
const descs = chartData.map(item => item.desc);
|
|
|
|
|
|
|
|
|
|
|
|
// 配置图表选项,与原页面保持一致
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
axisPointer: { type: 'shadow' },
|
|
|
|
|
|
formatter: (params: any) => {
|
|
|
|
|
|
const data = chartData[params[0].dataIndex];
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div style="padding: 4px;">
|
|
|
|
|
|
<div style="font-weight: bold; font-size: 14px;">${data.name}</div>
|
|
|
|
|
|
<div>CVSS评分: ${data.cvss}</div>
|
|
|
|
|
|
<div>漏洞数量: ${data.vulnCount}个</div>
|
|
|
|
|
|
<div>使用量: ${data.usage}</div>
|
|
|
|
|
|
<div>漏洞描述: ${data.desc}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: { left: '15%', right: '4%', bottom: '3%', top: '3%', containLabel: true },
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
name: 'CVSS评分',
|
|
|
|
|
|
nameTextStyle: { fontSize: 12 },
|
|
|
|
|
|
axisLabel: { fontSize: 12 },
|
|
|
|
|
|
max: 10
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: {
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
data: componentNames,
|
|
|
|
|
|
axisLabel: { fontSize: 12 },
|
|
|
|
|
|
inverse: true // 反转Y轴,让最高的值在顶部
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'CVSS评分',
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: cvssScores,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: (params: any) => {
|
|
|
|
|
|
const value = params.value;
|
|
|
|
|
|
if (value >= 9.0) return '#ef4444'; // 红色 - 严重
|
|
|
|
|
|
if (value >= 7.0) return '#f97316'; // 橙色 - 高危
|
|
|
|
|
|
if (value >= 4.0) return '#eab308'; // 黄色 - 中危
|
|
|
|
|
|
return '#22c55e'; // 绿色 - 低危
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
position: 'right',
|
|
|
|
|
|
formatter: '{c}',
|
|
|
|
|
|
fontSize: 12
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
chartInstance.setOption(option);
|
|
|
|
|
|
|
|
|
|
|
|
// 触发图表准备就绪事件
|
|
|
|
|
|
emit('chart-ready', chartInstance);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 组件挂载时初始化图表
|
2026-01-27 10:50:43 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await fetchComponentData(); // 获取高危组件数据
|
|
|
|
|
|
await nextTick();
|
2026-01-26 15:40:52 +08:00
|
|
|
|
initChart();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听数据变化,重新渲染图表
|
|
|
|
|
|
watch(() => props.data, () => {
|
|
|
|
|
|
initChart();
|
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
2026-01-27 10:50:43 +08:00
|
|
|
|
// 监听组件内部数据变化,重新渲染图表
|
|
|
|
|
|
watch(componentData, () => {
|
|
|
|
|
|
initChart();
|
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
2026-01-26 15:40:52 +08:00
|
|
|
|
// 组件卸载时清理图表实例
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
|
chartInstance.dispose();
|
|
|
|
|
|
chartInstance = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 页面大小变化时调整图表大小
|
|
|
|
|
|
const resizeHandler = () => {
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
|
chartInstance.resize();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 监听窗口大小变化
|
|
|
|
|
|
window.addEventListener('resize', resizeHandler);
|
|
|
|
|
|
|
|
|
|
|
|
// 在组件卸载时移除事件监听
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
window.removeEventListener('resize', resizeHandler);
|
|
|
|
|
|
});
|
|
|
|
|
|
</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.num-empty {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|