Files
Situation-Awareness-Platfor…/src/views/Jyh/security/components/CveTrendChart.vue

324 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="index-module__NV_5cW__chartCard chartCard__wide">
<div class="chartCard__header">
<h3 class="chartCard__title">CVE 月度趋势</h3>
<!-- 视图切换按钮用于切换全球和区域视图 -->
<div class="chartCard__actions">
<button
class="chartCard__actionBtn"
:class="{ 'chartCard__actionBtn--active': cveView === 'global' }"
@click="switchCveView('global')"
>
全球
</button>
<button
class="chartCard__actionBtn"
:class="{ 'chartCard__actionBtn--active': cveView === 'regional' }"
@click="switchCveView('regional')"
>
区域
</button>
</div>
</div>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.cveData"></div>
<div v-else id="CVEChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
// 视图切换状态变量global: 全球, regional: 区域)
const cveView = ref('global');
// 空数据标识
const isEmpty = ref({
cveData: false
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
CVEChart: null
});
// CVE图表数据
const cveData = {
// 全球CVE2024年实际数据基于Excel数据
global: {
cveCount: [98402, 418, 7025, 3711, 5115, 3228, 3044, 2957, 2592, 1455, 4826, 4661],
acceleration: [0, -99.58, 1580.62, -47.17, 37.83, -36.89, -5.70, -2.86, -12.34, -43.87, 231.68, -3.42] // 环比增长率(%
},
// 中国区域CVE2024年数据约占全球30%
regional: {
cveCount: [29521, 125, 2108, 1113, 1535, 968, 913, 887, 778, 437, 1448, 1398],
acceleration: [0, -99.58, 1580.62, -47.17, 37.83, -36.89, -5.70, -2.86, -12.34, -43.87, 231.68, -3.42] // 环比增长率(%
}
};
// CVE视图切换方法
const switchCveView = (view: 'global' | 'regional') => {
cveView.value = view;
nextTick(() => {
initCVEChart();
});
};
// 初始化CVE图表柱状图+折线图)
const initCVEChart = () => {
const el = document.getElementById('CVEChart');
if (!el) return;
// 延迟执行以确保DOM已渲染
setTimeout(() => {
const chartEl = document.getElementById('CVEChart');
if (!chartEl) return;
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.dispose();
}
const myChart = echarts.init(chartEl);
chartInstances.value.CVEChart = myChart;
// 获取当前视图的数据
const currentData = cveData[cveView.value === 'global' ? 'global' : 'regional'];
const cveCount = currentData.cveCount;
const acceleration = currentData.acceleration;
// 月份数据
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999'
}
},
formatter: (params: any) => {
let res = `<div>${params[0].name}</div>`;
params.forEach((p: any) => {
if (p.seriesName === 'CVE数量') {
res += `<div>${p.marker}${p.seriesName}: ${p.value}</div>`;
} else {
res += `<div>${p.marker}${p.seriesName}: ${p.value}%</div>`;
}
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: ['CVE数量', '环比增长率']
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
data: months,
axisPointer: {
type: 'shadow'
},
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: 'CVE数量',
nameTextStyle: { fontSize: 12, color: '#333' },
axisLabel: { fontSize: 12, color: '#666' },
position: 'left',
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
},
{
type: 'value',
name: '环比增长率(%)',
nameTextStyle: { fontSize: 12, color: '#333' },
axisLabel: { fontSize: 12, color: '#666' },
position: 'right',
splitLine: {
show: false
}
}
],
series: [
{
name: 'CVE数量',
type: 'bar',
data: cveCount,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#3b82f6' },
{ offset: 1, color: '#60a5fa' }
])
},
barWidth: 20,
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 10,
color: '#333'
}
},
{
name: '环比增长率',
type: 'line',
yAxisIndex: 1,
data: acceleration,
itemStyle: {
color: '#ef4444',
borderWidth: 2
},
lineStyle: {
width: 2,
color: '#ef4444'
},
symbol: 'circle',
symbolSize: 6,
smooth: true,
label: {
show: true,
position: 'top',
formatter: (params: any) => `${params.value}%`,
fontSize: 10,
color: '#333'
}
}
]
});
// 监听窗口大小变化,重新调整图表大小
window.addEventListener('resize', () => {
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.resize();
}
});
}, 0);
};
// 在组件挂载后初始化图表
onMounted(() => {
nextTick(() => {
initCVEChart();
});
});
// 在组件卸载前清理资源
onUnmounted(() => {
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.dispose();
}
});
defineExpose({
initCVEChart,
switchCveView,
cveView,
cveData
});
</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__wide {
grid-column: 1 / -1; /* 跨列显示 */
}
.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;
}
/* 图表卡片头部样式 */
.chartCard__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
/* 图表操作按钮样式 */
.chartCard__actions {
display: flex;
gap: 8px;
}
.chartCard__actionBtn {
padding: 4px 12px;
border: 1px solid #d1d5db;
background-color: #c1c7cd;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.chartCard__actionBtn:hover {
background-color: #f3f4f6;
}
.chartCard__actionBtn--active {
background-color: #3b82f6;
color: white;
border-color: #3b82f6;
}
.chartCard__actionBtn--active:hover {
background-color: #2563eb;
}
/* 与示例一致的空数据样式 */
.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>